Copy disabled (too large)
Download .txt
Showing preview only (10,705K chars total). Download the full file to get everything.
Repository: thomaskioko/tv-maniac
Branch: main
Commit: fd3340db192b
Files: 1716
Total size: 9.6 MB
Directory structure:
gitextract_xytv9ppo/
├── .editorconfig
├── .geminiignore
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ ├── actions/
│ │ ├── setup-android-release/
│ │ │ └── action.yml
│ │ ├── setup-gradle/
│ │ │ └── action.yml
│ │ ├── setup-ios/
│ │ │ └── action.yml
│ │ └── setup-ios-release/
│ │ └── action.yml
│ ├── release.yml
│ ├── renovate.json
│ └── workflows/
│ ├── baseline-profile.yml
│ ├── beta-release.yml
│ ├── ci.yml
│ ├── compare-screenshot.yml
│ ├── daily-build.yml
│ ├── nightly-integration-tests.yml
│ ├── promote-release.yml
│ ├── release.yml
│ └── store-screenshot.yml
├── .gitignore
├── .idea/
│ ├── codeStyles/
│ │ ├── Project.xml
│ │ └── codeStyleConfig.xml
│ └── dictionaries/
│ └── project.xml
├── .ruby-version
├── .swiftformat
├── .swiftlint.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── GEMINI.md
├── Gemfile
├── LICENSE
├── README.md
├── android-designsystem/
│ ├── build.gradle.kts
│ └── src/
│ ├── debug/
│ │ └── res/
│ │ └── values/
│ │ └── strings.xml
│ ├── main/
│ │ ├── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── compose/
│ │ │ ├── components/
│ │ │ │ ├── Background.kt
│ │ │ │ ├── BadgeChip.kt
│ │ │ │ ├── Buttons.kt
│ │ │ │ ├── Card.kt
│ │ │ │ ├── Chip.kt
│ │ │ │ ├── Dialogs.kt
│ │ │ │ ├── EmptyLayout.kt
│ │ │ │ ├── ErrorLayout.kt
│ │ │ │ ├── FilterChipSection.kt
│ │ │ │ ├── GradientScrim.kt
│ │ │ │ ├── Image.kt
│ │ │ │ ├── NavigationBar.kt
│ │ │ │ ├── NotificationRationaleContent.kt
│ │ │ │ ├── PosterPlaceholder.kt
│ │ │ │ ├── ProgressIndicator.kt
│ │ │ │ ├── ScanlineOverlay.kt
│ │ │ │ ├── SearchTextField.kt
│ │ │ │ ├── SegmentedProgressBar.kt
│ │ │ │ ├── SheetDragHandle.kt
│ │ │ │ ├── ShowLinearProgressIndicator.kt
│ │ │ │ ├── Snackbar.kt
│ │ │ │ ├── Text.kt
│ │ │ │ ├── TextTitlePill.kt
│ │ │ │ ├── TopBar.kt
│ │ │ │ ├── TvManiacBottomSheet.kt
│ │ │ │ └── TvManiacPreviewWrapperProvider.kt
│ │ │ ├── extensions/
│ │ │ │ ├── GradientExtensions.kt
│ │ │ │ ├── LazyListExtensions.kt
│ │ │ │ ├── PaddingValuesExtentions.kt
│ │ │ │ └── ScrimExtentions.kt
│ │ │ ├── theme/
│ │ │ │ ├── Background.kt
│ │ │ │ ├── Colors.kt
│ │ │ │ ├── Shape.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ └── util/
│ │ │ ├── AutoAdvanceLocal.kt
│ │ │ └── DynamicTheming.kt
│ │ └── res/
│ │ └── values/
│ │ └── strings.xml
│ └── test/
│ └── kotlin/
│ └── com/
│ └── thomaskioko/
│ └── tvmaniac/
│ └── compose/
│ └── roborazzi/
│ ├── NotificationRationaleContentScreenshotTest.kt
│ └── TvManiacSnackBarScreenshotTest.kt
├── api/
│ ├── tmdb/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── tmdb/
│ │ │ └── api/
│ │ │ ├── TmdbConfig.kt
│ │ │ ├── TmdbSeasonDetailsNetworkDataSource.kt
│ │ │ ├── TmdbShowDetailsNetworkDataSource.kt
│ │ │ ├── TmdbShowsNetworkDataSource.kt
│ │ │ └── model/
│ │ │ ├── CreditsResponse.kt
│ │ │ ├── EpisodesResponse.kt
│ │ │ ├── GenreResponse.kt
│ │ │ ├── ImagesResponse.kt
│ │ │ ├── LastEpisodeToAirResponse.kt
│ │ │ ├── NetworksResponse.kt
│ │ │ ├── NextEpisodeToAirResponse.kt
│ │ │ ├── SeasonsResponse.kt
│ │ │ ├── TmdbGenreResult.kt
│ │ │ ├── TmdbSeasonDetailsResponse.kt
│ │ │ ├── TmdbShowDetailsResponse.kt
│ │ │ ├── TmdbShowResponse.kt
│ │ │ ├── VideosResponse.kt
│ │ │ └── WatchProvidersResult.kt
│ │ └── implementation/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── androidMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── tmdb/
│ │ │ └── implementation/
│ │ │ └── TmdbPlatformBindingContainer.kt
│ │ ├── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── tmdb/
│ │ │ └── implementation/
│ │ │ ├── DefaultTmdbSeasonDetailsNetworkDataSource.kt
│ │ │ ├── DefaultTmdbShowDetailsNetworkDataSource.kt
│ │ │ ├── DefaultTmdbShowsNetworkDataSource.kt
│ │ │ ├── TmdbBindingContainer.kt
│ │ │ └── TmdbClient.kt
│ │ └── iosMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── tmdb/
│ │ └── implementation/
│ │ └── TmdbPlatformBindingContainer.kt
│ └── trakt/
│ ├── api/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── trakt/
│ │ └── api/
│ │ ├── TraktCalendarRemoteDataSource.kt
│ │ ├── TraktConfig.kt
│ │ ├── TraktEpisodeHistoryRemoteDataSource.kt
│ │ ├── TraktListRemoteDataSource.kt
│ │ ├── TraktShowsRemoteDataSource.kt
│ │ ├── TraktSyncRemoteDataSource.kt
│ │ ├── TraktTokenRemoteDataSource.kt
│ │ ├── TraktUserRemoteDataSource.kt
│ │ └── model/
│ │ ├── AccessTokenBody.kt
│ │ ├── RefreshAccessTokenBody.kt
│ │ ├── TraktAccessRefreshTokenResponse.kt
│ │ ├── TraktAccessTokenResponse.kt
│ │ ├── TraktAddShowRequest.kt
│ │ ├── TraktAddShowToListResponse.kt
│ │ ├── TraktCalendarResponse.kt
│ │ ├── TraktCreateListRequest.kt
│ │ ├── TraktCreateListResponse.kt
│ │ ├── TraktFollowedShowResponse.kt
│ │ ├── TraktGenreResponse.kt
│ │ ├── TraktLastActivitiesResponse.kt
│ │ ├── TraktNextEpisodeResponse.kt
│ │ ├── TraktPeopleResponse.kt
│ │ ├── TraktPersonalListsResponse.kt
│ │ ├── TraktRemoveShowFromListResponse.kt
│ │ ├── TraktSeasonEpisodesResponse.kt
│ │ ├── TraktSeasonsResponse.kt
│ │ ├── TraktShowsResponse.kt
│ │ ├── TraktSyncModels.kt
│ │ ├── TraktUserResponse.kt
│ │ ├── TraktUserStatsResponse.kt
│ │ ├── TraktVideosResponse.kt
│ │ └── TraktWatchedProgressResponse.kt
│ └── implementation/
│ ├── build.gradle.kts
│ └── src/
│ ├── androidMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── trakt/
│ │ └── service/
│ │ └── implementation/
│ │ └── TraktPlatformBindingContainer.kt
│ ├── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── trakt/
│ │ └── service/
│ │ └── implementation/
│ │ ├── TraktAuthPlugin.kt
│ │ ├── TraktBindingContainer.kt
│ │ ├── TraktHttpClient.kt
│ │ └── api/
│ │ ├── DefaultTraktCalendarRemoteDataSource.kt
│ │ ├── DefaultTraktEpisodeRemoteDataSource.kt
│ │ ├── DefaultTraktListRemoteDataSource.kt
│ │ ├── DefaultTraktShowsRemoteDataSource.kt
│ │ ├── DefaultTraktSyncRemoteDataSource.kt
│ │ ├── DefaultTraktTokenRemoteDataSource.kt
│ │ └── DefaultTraktUserRemoteDataSource.kt
│ ├── commonTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── trakt/
│ │ └── service/
│ │ └── implementation/
│ │ └── TraktAuthGuardPluginTest.kt
│ ├── iosMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── trakt/
│ │ └── service/
│ │ └── implementation/
│ │ └── TraktPlatformBindingContainer.kt
│ └── jvmTest/
│ ├── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── trakt/
│ │ └── service/
│ │ └── implementation/
│ │ ├── TestResourceLoader.jvm.kt
│ │ └── api/
│ │ └── DefaultTraktListRemoteDataSourceTest.kt
│ └── resources/
│ ├── trakt_add_show_response.json
│ ├── trakt_error_response.json
│ └── trakt_user_response.json
├── app/
│ ├── benchmark-rules.pro
│ ├── build.gradle.kts
│ ├── lint-baseline.xml
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── app/
│ │ └── test/
│ │ └── runner/
│ │ └── TvManiacInstrumentationRunner.kt
│ ├── debug/
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── app/
│ │ │ └── debug/
│ │ │ ├── DebugNotificationIconProvider.kt
│ │ │ ├── DebugNotificationInitializer.kt
│ │ │ └── di/
│ │ │ └── DebugNotificationInitializerBindingContainer.kt
│ │ └── res/
│ │ └── drawable/
│ │ ├── ic_app_launcher.xml
│ │ ├── ic_debug_bug.xml
│ │ └── ic_launcher_foreground.xml
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── generated/
│ │ │ └── baselineProfiles/
│ │ │ ├── baseline-prof.txt
│ │ │ └── startup-prof.txt
│ │ ├── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── app/
│ │ │ ├── MainActivity.kt
│ │ │ ├── TvManicApplication.kt
│ │ │ ├── di/
│ │ │ │ ├── ActivityGraph.kt
│ │ │ │ └── ApplicationGraph.kt
│ │ │ └── util/
│ │ │ ├── AppNotificationIconProvider.kt
│ │ │ └── TvManiacWorkerFactory.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── ic_app_launcher.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ └── ic_launcher_monochrome.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── values/
│ │ │ ├── colors.xml
│ │ │ └── themes.xml
│ │ ├── values-night/
│ │ │ ├── colors.xml
│ │ │ └── themes.xml
│ │ └── xml/
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ ├── sharedTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── app/
│ │ └── test/
│ │ ├── BaseAppFlowTest.kt
│ │ ├── TestAppComponent.kt
│ │ ├── TvManiacTestApplication.kt
│ │ └── compose/
│ │ ├── TvManiacTestActivity.kt
│ │ ├── flows/
│ │ │ ├── calendar/
│ │ │ │ └── CalendarFlowTest.kt
│ │ │ ├── discover/
│ │ │ │ ├── DiscoverToSeasonDetailsFlowTest.kt
│ │ │ │ └── DiscoverToShowDetailsFollowFlowTest.kt
│ │ │ ├── library/
│ │ │ │ └── LibraryFlowTest.kt
│ │ │ ├── search/
│ │ │ │ └── SearchFlowTest.kt
│ │ │ ├── seasons/
│ │ │ │ └── SeasonFlowTest.kt
│ │ │ ├── settings/
│ │ │ │ └── SettingsFlowTest.kt
│ │ │ ├── sheet/
│ │ │ │ └── EpisodeSheetFlowTest.kt
│ │ │ ├── showdetails/
│ │ │ │ └── ShowDetailsFeaturesFlowTest.kt
│ │ │ ├── upnext/
│ │ │ │ └── UpNextFlowTests.kt
│ │ │ └── userlists/
│ │ │ └── UserListFlowTests.kt
│ │ ├── journey/
│ │ │ ├── AuthenticatedUserJourneyTest.kt
│ │ │ └── UnauthenticatedUserJourneyTest.kt
│ │ ├── robot/
│ │ │ ├── CalendarRobot.kt
│ │ │ ├── DiscoverRobot.kt
│ │ │ ├── EpisodeSheetRobot.kt
│ │ │ ├── HomeRobot.kt
│ │ │ ├── LibraryRobot.kt
│ │ │ ├── ProfileRobot.kt
│ │ │ ├── ProgressRobot.kt
│ │ │ ├── RootRobot.kt
│ │ │ ├── SearchRobot.kt
│ │ │ ├── SeasonDetailsRobot.kt
│ │ │ ├── SettingsRobot.kt
│ │ │ └── ShowDetailsRobot.kt
│ │ └── stubs/
│ │ └── Scenarios.kt
│ └── test/
│ └── kotlin/
│ └── com/
│ └── thomaskioko/
│ └── tvmaniac/
│ └── app/
│ └── test/
│ └── graph/
│ ├── GraphFactories.kt
│ └── NavigationRouteTest.kt
├── benchmark/
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── kotlin/
│ └── com/
│ └── thomaskioko/
│ └── tvmaniac/
│ └── benchmark/
│ ├── Common.kt
│ ├── baselineprofile/
│ │ └── BaselineProfileGenerator.kt
│ └── benchmark/
│ └── StartupBenchmarks.kt
├── build.gradle.kts
├── cliff.toml
├── compose-stability.conf
├── core/
│ ├── appconfig/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── appconfig/
│ │ │ └── ApplicationInfo.kt
│ │ └── implementation/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── androidMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── appconfig/
│ │ │ └── AndroidAppConfigBindingContainer.kt
│ │ ├── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── appconfig/
│ │ │ ├── DefaultTmdbConfig.kt
│ │ │ └── DefaultTraktConfig.kt
│ │ └── iosMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── appconfig/
│ │ └── IosAppConfigBindingContainer.kt
│ ├── base/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── androidMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── core/
│ │ │ └── base/
│ │ │ └── di/
│ │ │ └── BaseAndroidBindingContainer.kt
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── core/
│ │ └── base/
│ │ ├── ActivityScope.kt
│ │ ├── AppInitializers.kt
│ │ ├── Initializer.kt
│ │ ├── Qualifiers.kt
│ │ ├── di/
│ │ │ ├── BaseBindingContainer.kt
│ │ │ └── InitializerMultibindings.kt
│ │ ├── extensions/
│ │ │ ├── Combine.kt
│ │ │ ├── DecomposeUtils.kt
│ │ │ ├── Lazy.kt
│ │ │ └── ParallelUtils.kt
│ │ ├── interactor/
│ │ │ └── Interactor.kt
│ │ └── model/
│ │ └── AppCoroutineDispatchers.kt
│ ├── connectivity/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── core/
│ │ │ └── connectivity/
│ │ │ └── api/
│ │ │ └── InternetConnectionChecker.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── androidMain/
│ │ │ │ ├── AndroidManifest.xml
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── connectivity/
│ │ │ │ └── implementation/
│ │ │ │ └── PlatformInternetConnectionChecker.android.kt
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── connectivity/
│ │ │ │ └── implementation/
│ │ │ │ └── PlatformInternetConnectionChecker.kt
│ │ │ ├── iosMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── connectivity/
│ │ │ │ └── implementation/
│ │ │ │ └── PlatformInternetConnectionChecker.ios.kt
│ │ │ └── jvmMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── core/
│ │ │ └── connectivity/
│ │ │ └── implementation/
│ │ │ └── PlatformInternetConnectionChecker.jvm.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── core/
│ │ └── connectivity/
│ │ └── testing/
│ │ └── FakeInternetConnectionChecker.kt
│ ├── imageloading/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── imageloading/
│ │ │ └── api/
│ │ │ └── ImageQualityProvider.kt
│ │ └── implementation/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── imageloading/
│ │ └── implementation/
│ │ ├── CoilImageLoaderFactory.kt
│ │ ├── CoilImageLoaderInitializer.kt
│ │ ├── DefaultImageQualityProvider.kt
│ │ ├── di/
│ │ │ ├── CoilImageLoaderInitializerBindingContainer.kt
│ │ │ └── ImageLoadingBindingContainer.kt
│ │ └── interceptors/
│ │ └── TmdbInterceptor.kt
│ ├── integration/
│ │ ├── infra/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── androidHostTest/
│ │ │ │ ├── AndroidManifest.xml
│ │ │ │ ├── kotlin/
│ │ │ │ │ └── com/
│ │ │ │ │ └── thomaskioko/
│ │ │ │ │ └── tvmaniac/
│ │ │ │ │ └── testing/
│ │ │ │ │ └── integration/
│ │ │ │ │ ├── EndpointsCatalogTest.kt
│ │ │ │ │ ├── FixtureLoaderTest.kt
│ │ │ │ │ └── MockEngineHandlerTest.kt
│ │ │ │ └── resources/
│ │ │ │ └── fixtures/
│ │ │ │ └── test/
│ │ │ │ └── hello.json
│ │ │ ├── androidMain/
│ │ │ │ ├── kotlin/
│ │ │ │ │ └── com/
│ │ │ │ │ └── thomaskioko/
│ │ │ │ │ └── tvmaniac/
│ │ │ │ │ └── testing/
│ │ │ │ │ └── integration/
│ │ │ │ │ ├── Endpoints.kt
│ │ │ │ │ ├── MockEngineHandler.kt
│ │ │ │ │ ├── SearchStubs.kt
│ │ │ │ │ ├── ShowFixtures.kt
│ │ │ │ │ ├── bindings/
│ │ │ │ │ │ ├── TestAuthBindingContainer.kt
│ │ │ │ │ │ ├── TestConnectivityBindingContainer.kt
│ │ │ │ │ │ ├── TestDateTimeBindingContainer.kt
│ │ │ │ │ │ ├── TestDispatcherBindingContainer.kt
│ │ │ │ │ │ ├── TestImageLoaderBindingContainer.kt
│ │ │ │ │ │ ├── TestInitializerBindingContainer.kt
│ │ │ │ │ │ ├── TestLoggerBindingContainer.kt
│ │ │ │ │ │ ├── TestNotificationBindingContainer.kt
│ │ │ │ │ │ ├── TestTmdbBindingContainer.kt
│ │ │ │ │ │ ├── TestTraktAuthManagerBindingContainer.kt
│ │ │ │ │ │ ├── TestTraktBindingContainer.kt
│ │ │ │ │ │ └── TestWorkerBindingContainer.kt
│ │ │ │ │ └── util/
│ │ │ │ │ └── FixtureLoader.kt
│ │ │ │ └── resources/
│ │ │ │ └── fixtures/
│ │ │ │ ├── empty_array.json
│ │ │ │ ├── tmdb/
│ │ │ │ │ ├── credits/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ ├── details/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ ├── discover/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ └── watchproviders/
│ │ │ │ │ ├── error.json
│ │ │ │ │ └── success.json
│ │ │ │ └── trakt/
│ │ │ │ ├── calendar/
│ │ │ │ │ ├── error.json
│ │ │ │ │ └── success.json
│ │ │ │ ├── episodes/
│ │ │ │ │ ├── season1/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ └── season2/
│ │ │ │ │ ├── error.json
│ │ │ │ │ └── success.json
│ │ │ │ ├── genres/
│ │ │ │ │ ├── error.json
│ │ │ │ │ └── success.json
│ │ │ │ ├── search/
│ │ │ │ │ ├── error.json
│ │ │ │ │ └── success.json
│ │ │ │ ├── seasons/
│ │ │ │ │ ├── error.json
│ │ │ │ │ └── success.json
│ │ │ │ ├── shows/
│ │ │ │ │ ├── details/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ ├── favorite/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ ├── people/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ ├── popular/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ ├── progress/
│ │ │ │ │ │ ├── refreshed/
│ │ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ │ └── success.json
│ │ │ │ │ │ └── watched/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ ├── related/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ ├── trending/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ └── videos/
│ │ │ │ │ ├── error.json
│ │ │ │ │ └── success.json
│ │ │ │ ├── sync/
│ │ │ │ │ ├── error.json
│ │ │ │ │ ├── history/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ └── success.json
│ │ │ │ └── users/
│ │ │ │ ├── lists/
│ │ │ │ │ ├── create/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ ├── error.json
│ │ │ │ │ ├── items/
│ │ │ │ │ │ ├── add/
│ │ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ │ └── success.json
│ │ │ │ │ │ └── remove/
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ └── success.json
│ │ │ │ │ └── success.json
│ │ │ │ ├── me/
│ │ │ │ │ ├── error.json
│ │ │ │ │ └── success.json
│ │ │ │ ├── stats/
│ │ │ │ │ ├── error.json
│ │ │ │ │ └── success.json
│ │ │ │ └── watchlist/
│ │ │ │ ├── error.json
│ │ │ │ └── success.json
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── testing/
│ │ │ │ └── di/
│ │ │ │ ├── FakeAppConfigBindingContainer.kt
│ │ │ │ └── TestScope.kt
│ │ │ ├── iosMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── testing/
│ │ │ │ └── di/
│ │ │ │ ├── FakeIosPlatformBindingContainer.kt
│ │ │ │ ├── RunTestWithGraph.kt
│ │ │ │ └── TestGraph.kt
│ │ │ ├── jvmAndIosMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── testing/
│ │ │ │ └── di/
│ │ │ │ └── FakeAppBindingContainer.kt
│ │ │ ├── jvmMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── testing/
│ │ │ │ └── di/
│ │ │ │ ├── RunTestWithGraph.kt
│ │ │ │ ├── TestGraph.kt
│ │ │ │ └── TestJvmPlatformBindingContainer.kt
│ │ │ └── jvmTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── testing/
│ │ │ └── di/
│ │ │ └── TestJvmGraphTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── androidMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── testing/
│ │ └── integration/
│ │ └── ui/
│ │ ├── BaseRobot.kt
│ │ └── SystemDialogUtil.kt
│ ├── locale/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── locale/
│ │ │ └── api/
│ │ │ ├── Language.kt
│ │ │ └── LocaleProvider.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── androidDeviceTest/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── locale/
│ │ │ │ └── implementation/
│ │ │ │ └── PlatformLocaleProviderAndroidTest.kt
│ │ │ ├── androidMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── locale/
│ │ │ │ └── implementation/
│ │ │ │ └── PlatformLocaleProvider.android.kt
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── locale/
│ │ │ │ └── implementation/
│ │ │ │ ├── DefaultLocaleProvider.kt
│ │ │ │ └── PlatformLocaleProvider.kt
│ │ │ ├── commonTest/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── locale/
│ │ │ │ └── implementation/
│ │ │ │ └── PlatformLocaleProviderTest.kt
│ │ │ ├── iosMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── locale/
│ │ │ │ └── implementation/
│ │ │ │ └── PlatformLocaleProvider.ios.kt
│ │ │ ├── iosTest/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── locale/
│ │ │ │ └── implementation/
│ │ │ │ └── PlatformLocaleProviderIosTest.kt
│ │ │ ├── jvmMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── locale/
│ │ │ │ └── implementation/
│ │ │ │ └── PlatformLocaleProvider.jvm.kt
│ │ │ └── jvmTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── locale/
│ │ │ └── implementation/
│ │ │ └── PlatformLocaleProviderJvmTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── locale/
│ │ └── testing/
│ │ └── FakeLocaleProvider.kt
│ ├── logger/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── logger/
│ │ │ │ ├── CrashReporter.kt
│ │ │ │ └── Logger.kt
│ │ │ └── iosMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── core/
│ │ │ └── logger/
│ │ │ ├── CrashReportingBridge.kt
│ │ │ └── CrashReportingBridgeHolder.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── androidMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── logger/
│ │ │ │ ├── AndroidCrashReporter.kt
│ │ │ │ └── AndroidLoggerBindingContainer.kt
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── logger/
│ │ │ │ ├── CompositeLogger.kt
│ │ │ │ ├── FirebaseCrashLogger.kt
│ │ │ │ ├── KermitLogger.kt
│ │ │ │ └── LoggingInitializer.kt
│ │ │ └── iosMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── core/
│ │ │ └── logger/
│ │ │ ├── IosCrashReporter.kt
│ │ │ ├── IosCrashReporterBindingContainer.kt
│ │ │ └── NoOpCrashReportingBridge.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── core/
│ │ └── logger/
│ │ └── fixture/
│ │ ├── FakeCrashReporter.kt
│ │ └── FakeLogger.kt
│ ├── network-util/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── networkutil/
│ │ │ │ └── api/
│ │ │ │ ├── ApiRateLimiter.kt
│ │ │ │ ├── extensions/
│ │ │ │ │ ├── ApiRateLimiterExtensions.kt
│ │ │ │ │ ├── ApiResponseExtensions.kt
│ │ │ │ │ ├── InternetConnectionPlugin.kt
│ │ │ │ │ └── StoreExtensions.kt
│ │ │ │ └── model/
│ │ │ │ ├── ApiExceptions.kt
│ │ │ │ ├── ApiResponse.kt
│ │ │ │ ├── AuthenticationException.kt
│ │ │ │ ├── HttpExceptions.kt
│ │ │ │ ├── NoInternetException.kt
│ │ │ │ ├── SyncError.kt
│ │ │ │ └── SyncException.kt
│ │ │ ├── commonTest/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── networkutil/
│ │ │ │ └── api/
│ │ │ │ └── model/
│ │ │ │ └── ThrowableToSyncErrorTest.kt
│ │ │ └── jvmTest/
│ │ │ ├── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── networkutil/
│ │ │ │ └── api/
│ │ │ │ └── extensions/
│ │ │ │ ├── ApiResponseExtensionsTest.kt
│ │ │ │ ├── InternetConnectionPluginTest.kt
│ │ │ │ └── TestResourceLoader.jvm.kt
│ │ │ └── resources/
│ │ │ ├── error_response.json
│ │ │ └── success_response.json
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── networkutil/
│ │ │ │ └── ratelimit/
│ │ │ │ └── AdaptiveApiRateLimiter.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── core/
│ │ │ └── networkutil/
│ │ │ ├── model/
│ │ │ │ └── SyncErrorTest.kt
│ │ │ └── ratelimit/
│ │ │ └── AdaptiveApiRateLimiterTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── core/
│ │ └── networkutil/
│ │ └── testing/
│ │ └── FakeApiRateLimiter.kt
│ ├── notifications/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── androidMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── notifications/
│ │ │ │ └── api/
│ │ │ │ └── NotificationIconProvider.kt
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── core/
│ │ │ └── notifications/
│ │ │ └── api/
│ │ │ ├── EpisodeNotification.kt
│ │ │ ├── NotificationChannel.kt
│ │ │ └── NotificationManager.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── androidMain/
│ │ │ │ ├── AndroidManifest.xml
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── notifications/
│ │ │ │ └── implementation/
│ │ │ │ ├── AndroidNotificationManager.kt
│ │ │ │ ├── BootCompletedReceiver.kt
│ │ │ │ ├── DebugNotificationManager.kt
│ │ │ │ ├── EpisodeNotificationReceiver.kt
│ │ │ │ ├── PendingNotificationsStore.kt
│ │ │ │ └── model/
│ │ │ │ └── StoredNotification.kt
│ │ │ └── iosMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── core/
│ │ │ └── notifications/
│ │ │ └── implementation/
│ │ │ └── IosNotificationManager.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── core/
│ │ └── notifications/
│ │ └── testing/
│ │ └── FakeNotificationManager.kt
│ ├── paging/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── core/
│ │ └── paging/
│ │ ├── CommonPagingConfig.kt
│ │ ├── KeyedQueryPagingSource.kt
│ │ ├── OffsetQueryPagingSource.kt
│ │ ├── PaginatedRemoteMediator.kt
│ │ └── QueryPagingSource.kt
│ ├── screenshot-tests/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── screenshottests/
│ │ └── RoborazziScreenshotUtil.kt
│ ├── tasks/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── core/
│ │ │ └── tasks/
│ │ │ └── api/
│ │ │ ├── BackgroundTaskScheduler.kt
│ │ │ ├── BackgroundWorker.kt
│ │ │ ├── PeriodicTaskRequest.kt
│ │ │ ├── TaskConstraints.kt
│ │ │ ├── WorkerFactory.kt
│ │ │ └── WorkerResult.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── androidMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── tasks/
│ │ │ │ └── implementation/
│ │ │ │ ├── AndroidTaskScheduler.kt
│ │ │ │ ├── SchedulerDispatchWorker.kt
│ │ │ │ └── di/
│ │ │ │ └── WorkManagerBindingContainer.kt
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── core/
│ │ │ │ └── tasks/
│ │ │ │ └── implementation/
│ │ │ │ └── DefaultWorkerFactory.kt
│ │ │ └── iosMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── core/
│ │ │ └── tasks/
│ │ │ └── implementation/
│ │ │ └── IosTaskScheduler.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── core/
│ │ └── tasks/
│ │ └── testing/
│ │ └── FakeBackgroundTaskScheduler.kt
│ ├── test-tags/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── testtags/
│ │ ├── calendar/
│ │ │ └── CalendarTestTags.kt
│ │ ├── component/
│ │ │ └── DesignComponentTestTags.kt
│ │ ├── discover/
│ │ │ └── DiscoverTestTags.kt
│ │ ├── episodesheet/
│ │ │ └── EpisodeSheetTestTags.kt
│ │ ├── home/
│ │ │ └── HomeTestTags.kt
│ │ ├── library/
│ │ │ └── LibraryTestTags.kt
│ │ ├── moreshows/
│ │ │ └── MoreShowsTestTags.kt
│ │ ├── notifications/
│ │ │ └── NotificationRationaleTestTags.kt
│ │ ├── profile/
│ │ │ └── ProfileTestTags.kt
│ │ ├── progress/
│ │ │ └── ProgressTestTags.kt
│ │ ├── search/
│ │ │ └── SearchTestTags.kt
│ │ ├── seasondetails/
│ │ │ └── SeasonDetailsTestTags.kt
│ │ ├── settings/
│ │ │ └── SettingsTestTags.kt
│ │ ├── showdetails/
│ │ │ └── ShowDetailsTestTags.kt
│ │ └── upnext/
│ │ └── UpNextTestTags.kt
│ ├── util/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── util/
│ │ │ └── api/
│ │ │ ├── AppUtils.kt
│ │ │ ├── DateTimeProvider.kt
│ │ │ ├── FormatterUtil.kt
│ │ │ └── ItemSyncer.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── androidMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── util/
│ │ │ │ ├── AndroidAppUtils.kt
│ │ │ │ └── AndroidFormatterUtil.kt
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── util/
│ │ │ │ ├── DateTimeBindingContainer.kt
│ │ │ │ └── DefaultDateTimeProvider.kt
│ │ │ ├── commonTest/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── util/
│ │ │ │ └── DefaultDateTimeProviderTest.kt
│ │ │ ├── iosMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── util/
│ │ │ │ ├── IosAppUtils.kt
│ │ │ │ └── IosFormatterUtil.kt
│ │ │ ├── iosTest/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── util/
│ │ │ │ └── IosFormatterUtilTest.kt
│ │ │ └── test/
│ │ │ └── java/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── util/
│ │ │ └── AndroidFormatterUtilTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── util/
│ │ │ └── testing/
│ │ │ ├── FakeApplicationInfo.kt
│ │ │ ├── FakeDateTimeProvider.kt
│ │ │ ├── FakeFormatterUtil.kt
│ │ │ └── FlakyTests.kt
│ │ └── jvmAndroidMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── util/
│ │ └── testing/
│ │ └── FlakyTestRule.kt
│ └── view/
│ ├── build.gradle.kts
│ └── src/
│ └── commonMain/
│ └── kotlin/
│ └── com/
│ └── thomaskioko/
│ └── tvmaniac/
│ └── core/
│ └── view/
│ ├── ErrorToStringMapper.kt
│ ├── InvokeStatus.kt
│ ├── ObservableLoadingCounter.kt
│ └── UiMessage.kt
├── data/
│ ├── calendar/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── calendar/
│ │ │ ├── CalendarDao.kt
│ │ │ ├── CalendarEntry.kt
│ │ │ └── CalendarRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── calendar/
│ │ │ └── implementation/
│ │ │ ├── CalendarStore.kt
│ │ │ ├── DefaultCalendarDao.kt
│ │ │ └── DefaultCalendarRepository.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── calendar/
│ │ └── testing/
│ │ └── FakeCalendarRepository.kt
│ ├── cast/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── cast/
│ │ │ └── api/
│ │ │ ├── CastDao.kt
│ │ │ └── CastRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── cast/
│ │ │ └── implementation/
│ │ │ ├── DefaultCastDao.kt
│ │ │ ├── DefaultCastRepository.kt
│ │ │ ├── ShowCastResult.kt
│ │ │ └── ShowCastStore.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── cast/
│ │ └── testing/
│ │ └── FakeCastRepository.kt
│ ├── database/
│ │ ├── sqldelight/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── androidMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── db/
│ │ │ │ └── DatabasePlatformBindingContainer.kt
│ │ │ ├── commonMain/
│ │ │ │ └── sqldelight/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ ├── db/
│ │ │ │ │ ├── Calendar.sq
│ │ │ │ │ ├── Cast.sq
│ │ │ │ │ ├── EpisodeImage.sq
│ │ │ │ │ ├── Episodes.sq
│ │ │ │ │ ├── FeaturedShows.sq
│ │ │ │ │ ├── FollowedShows.sq
│ │ │ │ │ ├── GenreShows.sq
│ │ │ │ │ ├── Genres.sq
│ │ │ │ │ ├── LastRequests.sq
│ │ │ │ │ ├── Library.sq
│ │ │ │ │ ├── NextEpisodes.sq
│ │ │ │ │ ├── PopularShows.sq
│ │ │ │ │ ├── RecommendedShows.sq
│ │ │ │ │ ├── SeasonImages.sq
│ │ │ │ │ ├── SeasonVideos.sq
│ │ │ │ │ ├── Seasons.sq
│ │ │ │ │ ├── ShowGenres.sq
│ │ │ │ │ ├── ShowMetadata.sq
│ │ │ │ │ ├── ShowsLastWatched.sq
│ │ │ │ │ ├── ShowsNextToWatch.sq
│ │ │ │ │ ├── SimilarShows.sq
│ │ │ │ │ ├── Stats.sq
│ │ │ │ │ ├── TopratedShows.sq
│ │ │ │ │ ├── Trailers.sq
│ │ │ │ │ ├── TraktGenres.sq
│ │ │ │ │ ├── TraktLastActivity.sq
│ │ │ │ │ ├── TraktListShows.sq
│ │ │ │ │ ├── TraktLists.sq
│ │ │ │ │ ├── TrendingShows.sq
│ │ │ │ │ ├── TvShow.sq
│ │ │ │ │ ├── UpcomingShows.sq
│ │ │ │ │ ├── User.sq
│ │ │ │ │ ├── WatchProviders.sq
│ │ │ │ │ └── WatchedEpisodes.sq
│ │ │ │ └── migrations/
│ │ │ │ ├── 1.sqm
│ │ │ │ ├── 10.sqm
│ │ │ │ ├── 11.sqm
│ │ │ │ ├── 12.sqm
│ │ │ │ ├── 13.sqm
│ │ │ │ ├── 14.sqm
│ │ │ │ ├── 15.sqm
│ │ │ │ ├── 16.sqm
│ │ │ │ ├── 17.sqm
│ │ │ │ ├── 18.sqm
│ │ │ │ ├── 19.sqm
│ │ │ │ ├── 2.sqm
│ │ │ │ ├── 20.sqm
│ │ │ │ ├── 21.sqm
│ │ │ │ ├── 22.sqm
│ │ │ │ ├── 23.sqm
│ │ │ │ ├── 3.sqm
│ │ │ │ ├── 4.sqm
│ │ │ │ ├── 5.sqm
│ │ │ │ ├── 6.sqm
│ │ │ │ ├── 7.sqm
│ │ │ │ ├── 8.sqm
│ │ │ │ └── 9.sqm
│ │ │ ├── iosMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── db/
│ │ │ │ └── DatabasePlatformBindingContainer.kt
│ │ │ └── jvmTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── db/
│ │ │ ├── Migration22TraktListShowsTest.kt
│ │ │ ├── Migration23DropParentFkTest.kt
│ │ │ ├── SchemaCreateTest.kt
│ │ │ └── util/
│ │ │ └── MigrationTestUtil.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── androidMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── database/
│ │ │ └── test/
│ │ │ └── BaseDatabaseTest.android.kt
│ │ ├── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── database/
│ │ │ └── test/
│ │ │ └── BaseDatabaseTest.kt
│ │ ├── iosMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── database/
│ │ │ └── test/
│ │ │ └── BaseDatabaseTest.ios.kt
│ │ └── jvmMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── database/
│ │ └── test/
│ │ └── BaseDatabaseTest.jvm.kt
│ ├── datastore/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── datastore/
│ │ │ └── api/
│ │ │ ├── AppTheme.kt
│ │ │ ├── DatastoreRepository.kt
│ │ │ ├── ImageQuality.kt
│ │ │ └── ListStyle.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ ├── src/
│ │ │ │ ├── androidMain/
│ │ │ │ │ └── kotlin/
│ │ │ │ │ └── com/
│ │ │ │ │ └── thomaskioko/
│ │ │ │ │ └── tvmaniac/
│ │ │ │ │ └── datastore/
│ │ │ │ │ └── implementation/
│ │ │ │ │ └── DataStorePlatformBindingContainer.kt
│ │ │ │ ├── commonMain/
│ │ │ │ │ └── kotlin/
│ │ │ │ │ └── com/
│ │ │ │ │ └── thomaskioko/
│ │ │ │ │ └── tvmaniac/
│ │ │ │ │ └── datastore/
│ │ │ │ │ └── implementation/
│ │ │ │ │ ├── DataStoreHelper.kt
│ │ │ │ │ └── DefaultDatastoreRepository.kt
│ │ │ │ ├── commonTest/
│ │ │ │ │ └── kotlin/
│ │ │ │ │ └── com/
│ │ │ │ │ └── thomaskioko/
│ │ │ │ │ └── tvmaniac/
│ │ │ │ │ └── datastore/
│ │ │ │ │ └── implemetation/
│ │ │ │ │ └── DatastoreRepositoryTest.kt
│ │ │ │ └── iosMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── datastore/
│ │ │ │ └── implementation/
│ │ │ │ └── DataStorePlatformBindingContainer.kt
│ │ │ └── test.preferences_pb
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── datastore/
│ │ └── testing/
│ │ └── FakeDatastoreRepository.kt
│ ├── episode/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── episodes/
│ │ │ └── api/
│ │ │ ├── EpisodeRepository.kt
│ │ │ ├── EpisodeWatchesDataSource.kt
│ │ │ ├── EpisodesDao.kt
│ │ │ ├── NextEpisodeDao.kt
│ │ │ ├── WatchedEpisodeDao.kt
│ │ │ ├── WatchedEpisodeEntry.kt
│ │ │ ├── WatchedEpisodeSyncRepository.kt
│ │ │ └── model/
│ │ │ ├── EpisodeExtensions.kt
│ │ │ ├── EpisodeWatchParams.kt
│ │ │ ├── LastWatchedEpisode.kt
│ │ │ ├── SeasonWatchProgress.kt
│ │ │ ├── ShowWatchProgress.kt
│ │ │ ├── UnwatchedEpisode.kt
│ │ │ ├── UpcomingEpisode.kt
│ │ │ ├── WatchProgress.kt
│ │ │ └── WatchedEpisode.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── episodes/
│ │ │ │ └── implementation/
│ │ │ │ ├── DefaultEpisodeRepository.kt
│ │ │ │ ├── DefaultWatchedEpisodeSyncRepository.kt
│ │ │ │ ├── EpisodeWatchesLastRequestStore.kt
│ │ │ │ ├── TraktEpisodeWatchesDataSource.kt
│ │ │ │ ├── UpcomingEpisodesStore.kt
│ │ │ │ ├── dao/
│ │ │ │ │ ├── DefaultEpisodesDao.kt
│ │ │ │ │ ├── DefaultNextEpisodeDao.kt
│ │ │ │ │ └── DefaultWatchedEpisodeDao.kt
│ │ │ │ └── model/
│ │ │ │ └── NextEpisodeKey.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── episodes/
│ │ │ └── implementation/
│ │ │ ├── DefaultEpisodeRepositoryTest.kt
│ │ │ ├── DefaultEpisodesDaoTest.kt
│ │ │ ├── DefaultNextEpisodeDaoTest.kt
│ │ │ ├── DefaultWatchedEpisodeDaoTest.kt
│ │ │ ├── EpisodesCacheTest.kt
│ │ │ └── MockData.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── episodes/
│ │ └── testing/
│ │ ├── FakeEpisodeRepository.kt
│ │ ├── FakeEpisodeWatchesDataSource.kt
│ │ └── FakeWatchedEpisodeSyncRepository.kt
│ ├── featuredshows/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── featuredshows/
│ │ │ └── api/
│ │ │ ├── FeaturedShowsDao.kt
│ │ │ ├── FeaturedShowsRepository.kt
│ │ │ └── interactor/
│ │ │ └── FeaturedShowsInteractor.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── data/
│ │ │ │ └── featuredshows/
│ │ │ │ └── implementation/
│ │ │ │ ├── DefaultFeaturedShowsDao.kt
│ │ │ │ ├── DefaultFeaturedShowsRepository.kt
│ │ │ │ └── FeaturedShowsStore.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── featuredshows/
│ │ │ └── implementation/
│ │ │ └── DefaultFeaturedShowsDaoTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── featuredshows/
│ │ └── testing/
│ │ └── FakeFeaturedShowsRepository.kt
│ ├── followedshows/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── followedshows/
│ │ │ └── api/
│ │ │ ├── FollowedShowEntry.kt
│ │ │ ├── FollowedShowsDao.kt
│ │ │ ├── FollowedShowsRepository.kt
│ │ │ └── PendingAction.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── followedshows/
│ │ │ │ └── implementation/
│ │ │ │ ├── DefaultFollowedShowsDao.kt
│ │ │ │ └── DefaultFollowedShowsRepository.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── followedshows/
│ │ │ └── implementation/
│ │ │ ├── DefaultFollowedShowsDaoTest.kt
│ │ │ └── DefaultFollowedShowsRepositoryTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── followedshows/
│ │ └── testing/
│ │ ├── FakeFollowedShowsDao.kt
│ │ └── FakeFollowedShowsRepository.kt
│ ├── genre/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── genre/
│ │ │ ├── GenreDao.kt
│ │ │ ├── GenreRepository.kt
│ │ │ ├── ShowGenresEntity.kt
│ │ │ ├── TraktGenreDao.kt
│ │ │ └── model/
│ │ │ ├── GenreShowCategory.kt
│ │ │ ├── GenreShowsStoreKey.kt
│ │ │ ├── GenreWithShowsEntity.kt
│ │ │ └── TraktGenreEntity.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── genre/
│ │ │ ├── DefaultGenreDao.kt
│ │ │ ├── DefaultGenreRepository.kt
│ │ │ ├── DefaultTraktGenreDao.kt
│ │ │ ├── GenrePosterStore.kt
│ │ │ ├── GenreShowsStore.kt
│ │ │ ├── GenreStore.kt
│ │ │ ├── ShowsByGenreIdStore.kt
│ │ │ └── TraktGenresStore.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── genre/
│ │ └── FakeGenreRepository.kt
│ ├── library/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── library/
│ │ │ ├── LibraryDao.kt
│ │ │ ├── LibraryRepository.kt
│ │ │ └── model/
│ │ │ ├── LibraryItem.kt
│ │ │ ├── LibrarySortOption.kt
│ │ │ └── WatchProvider.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── library/
│ │ │ └── implementation/
│ │ │ ├── DefaultLibraryDao.kt
│ │ │ ├── DefaultLibraryRepository.kt
│ │ │ └── LibraryStore.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── library/
│ │ └── testing/
│ │ └── FakeLibraryRepository.kt
│ ├── popularshows/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── popularshows/
│ │ │ └── api/
│ │ │ ├── PopularShowsDao.kt
│ │ │ ├── PopularShowsInteractor.kt
│ │ │ └── PopularShowsRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── data/
│ │ │ │ └── popularshows/
│ │ │ │ └── implementation/
│ │ │ │ ├── DefaultPopularShowsDao.kt
│ │ │ │ ├── DefaultPopularShowsRepository.kt
│ │ │ │ └── PopularShowsStore.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── popularshows/
│ │ │ └── implementation/
│ │ │ └── DefaultPopularShowsDaoTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── popularshows/
│ │ └── testing/
│ │ └── FakePopularShowsRepository.kt
│ ├── recommendedshows/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── recommendedshows/
│ │ │ └── api/
│ │ │ ├── RecommendedShowsDao.kt
│ │ │ ├── RecommendedShowsParams.kt
│ │ │ └── RecommendedShowsRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── recommendedshows/
│ │ │ └── implementation/
│ │ │ ├── DefaultRecommendedShowsDao.kt
│ │ │ ├── DefaultRecommendedShowsRepository.kt
│ │ │ ├── RecommendedShowResult.kt
│ │ │ └── RecommendedShowsStore.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── recommendedshows/
│ │ └── testing/
│ │ └── FakeRecommendedShowsRepository.kt
│ ├── request-manager/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── resourcemanager/
│ │ │ └── api/
│ │ │ ├── RequestManagerRepository.kt
│ │ │ └── RequestTypeConfig.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── resourcemanager/
│ │ │ │ └── implementation/
│ │ │ │ └── DefaultRequestManagerRepository.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── resourcemanager/
│ │ │ └── implementation/
│ │ │ ├── CacheValidationTest.kt
│ │ │ └── DefaultRequestManagerRepositoryTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── requestmanager/
│ │ └── testing/
│ │ └── FakeRequestManagerRepository.kt
│ ├── search/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── search/
│ │ │ └── api/
│ │ │ └── SearchRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── search/
│ │ │ └── implementation/
│ │ │ ├── DefaultSearchRepository.kt
│ │ │ ├── SearchShowResult.kt
│ │ │ └── SearchShowStore.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── search/
│ │ └── testing/
│ │ └── FakeSearchRepository.kt
│ ├── seasondetails/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── seasondetails/
│ │ │ └── api/
│ │ │ ├── SeasonDetailsDao.kt
│ │ │ ├── SeasonDetailsParam.kt
│ │ │ ├── SeasonDetailsRepository.kt
│ │ │ └── model/
│ │ │ ├── ContinueTrackingResult.kt
│ │ │ ├── EpisodeDetails.kt
│ │ │ └── SeasonDetailsWithEpisodes.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── seasondetails/
│ │ │ └── implementation/
│ │ │ ├── DefaultSeasonDetailsDao.kt
│ │ │ ├── DefaultSeasonDetailsRepository.kt
│ │ │ ├── SeasonDetailsResponse.kt
│ │ │ └── SeasonDetailsStore.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── seasondetails/
│ │ └── testing/
│ │ └── FakeSeasonDetailsRepository.kt
│ ├── seasons/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── seasons/
│ │ │ └── api/
│ │ │ ├── FollowedShowSeason.kt
│ │ │ ├── SeasonsDao.kt
│ │ │ ├── SeasonsEpisodesSyncRepository.kt
│ │ │ └── SeasonsRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── seasons/
│ │ │ └── implementation/
│ │ │ ├── DefaultSeasonsDao.kt
│ │ │ ├── DefaultSeasonsEpisodesSyncRepository.kt
│ │ │ ├── DefaultSeasonsRepository.kt
│ │ │ └── SeasonsWithEpisodesStore.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── seasons/
│ │ └── testing/
│ │ └── FakeSeasonsRepository.kt
│ ├── showdetails/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── showdetails/
│ │ │ └── api/
│ │ │ ├── ShowDetailsDao.kt
│ │ │ └── ShowDetailsRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── showdetails/
│ │ │ └── implementation/
│ │ │ ├── DefaultShowDetailsDao.kt
│ │ │ ├── DefaultShowDetailsRepository.kt
│ │ │ ├── ShowDetailsResponse.kt
│ │ │ └── ShowDetailsStore.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── showdetails/
│ │ └── testing/
│ │ └── FakeShowDetailsRepository.kt
│ ├── shows/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── shows/
│ │ │ │ └── api/
│ │ │ │ ├── MergeShowUtil.kt
│ │ │ │ ├── TvShowsDao.kt
│ │ │ │ └── model/
│ │ │ │ ├── Category.kt
│ │ │ │ ├── ShowDefaults.kt
│ │ │ │ └── ShowEntity.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── shows/
│ │ │ └── api/
│ │ │ ├── MockData.kt
│ │ │ └── TvShowCacheTest.kt
│ │ └── implementation/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── shows/
│ │ └── implementation/
│ │ └── DefaultTvShowsDao.kt
│ ├── similar/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── similar/
│ │ │ └── api/
│ │ │ ├── SimilarShowsDao.kt
│ │ │ └── SimilarShowsRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── similar/
│ │ │ └── implementation/
│ │ │ ├── DefaultSimilarShowsDao.kt
│ │ │ ├── DefaultSimilarShowsRepository.kt
│ │ │ ├── SimilarParams.kt
│ │ │ ├── SimilarShowResult.kt
│ │ │ └── SimilarShowStore.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── similar/
│ │ └── testing/
│ │ └── FakeSimilarShowsRepository.kt
│ ├── sync-activity/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── syncactivity/
│ │ │ └── api/
│ │ │ ├── TraktActivityDao.kt
│ │ │ ├── TraktActivityRepository.kt
│ │ │ └── model/
│ │ │ ├── ActivityType.kt
│ │ │ └── TraktLastActivity.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── syncactivity/
│ │ │ │ └── implementation/
│ │ │ │ ├── DefaultTraktActivityDao.kt
│ │ │ │ ├── DefaultTraktActivityRepository.kt
│ │ │ │ └── TraktActivityStore.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── syncactivity/
│ │ │ └── implementation/
│ │ │ └── DefaultTraktActivityDaoTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── syncactivity/
│ │ └── testing/
│ │ ├── FakeTraktActivityDao.kt
│ │ └── FakeTraktActivityRepository.kt
│ ├── topratedshows/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── topratedshows/
│ │ │ └── data/
│ │ │ └── api/
│ │ │ ├── TopRatedShowsDao.kt
│ │ │ ├── TopRatedShowsInteractor.kt
│ │ │ └── TopRatedShowsRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── toprated/
│ │ │ │ └── data/
│ │ │ │ └── implementation/
│ │ │ │ ├── DefaultTopRatedShowsDao.kt
│ │ │ │ ├── DefaultTopRatedShowsRepository.kt
│ │ │ │ ├── TopRatedShowWithImages.kt
│ │ │ │ └── TopRatedShowsStore.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── toprated/
│ │ │ └── data/
│ │ │ └── implementation/
│ │ │ └── DefaultTopRatedShowsDaoTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── topratedshows/
│ │ └── testing/
│ │ └── FakeTopRatedShowsRepository.kt
│ ├── trailers/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com.thomaskioko.tvmaniac.data.trailers.implementation/
│ │ │ ├── TrailerDao.kt
│ │ │ └── TrailerRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── trailers/
│ │ │ └── implementation/
│ │ │ ├── DefaultTrailerDao.kt
│ │ │ ├── DefaultTrailerRepository.kt
│ │ │ └── TrailerStore.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── trailers/
│ │ └── testing/
│ │ ├── FakeTrailerRepository.kt
│ │ └── MockData.kt
│ ├── traktauth/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── traktauth/
│ │ │ └── api/
│ │ │ ├── AuthError.kt
│ │ │ ├── AuthState.kt
│ │ │ ├── AuthStore.kt
│ │ │ ├── RefreshTokenResult.kt
│ │ │ ├── TokenRefreshResult.kt
│ │ │ ├── TraktAuthManager.kt
│ │ │ ├── TraktAuthRepository.kt
│ │ │ ├── TraktAuthState.kt
│ │ │ └── TraktRefreshTokenAction.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── androidMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── traktauth/
│ │ │ │ └── implementation/
│ │ │ │ ├── AndroidAuthStore.kt
│ │ │ │ ├── AndroidTraktAuthManager.kt
│ │ │ │ ├── TraktActivityResultContract.kt
│ │ │ │ └── di/
│ │ │ │ └── TraktAuthAndroidBindingContainer.kt
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── traktauth/
│ │ │ │ └── implementation/
│ │ │ │ ├── DefaultTraktAuthRepository.kt
│ │ │ │ ├── DefaultTraktRefreshTokenAction.kt
│ │ │ │ ├── TokenRefreshInitializer.kt
│ │ │ │ ├── TokenRefreshWorker.kt
│ │ │ │ └── di/
│ │ │ │ └── TokenRefreshInitializerBindingContainer.kt
│ │ │ └── iosMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── traktauth/
│ │ │ └── implementation/
│ │ │ ├── DefaultIOSTraktAuthManager.kt
│ │ │ └── IosAuthStore.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── traktauth/
│ │ └── testing/
│ │ ├── FakeAuthStore.kt
│ │ ├── FakeTraktAuthManager.kt
│ │ ├── FakeTraktAuthRepository.kt
│ │ └── FakeTraktRefreshTokenAction.kt
│ ├── traktlists/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── traktlists/
│ │ │ └── api/
│ │ │ ├── TraktList.kt
│ │ │ ├── TraktListDao.kt
│ │ │ ├── TraktListEntity.kt
│ │ │ ├── TraktListRepository.kt
│ │ │ ├── TraktListShowDao.kt
│ │ │ └── TraktListShowEntry.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── traktlists/
│ │ │ └── implementation/
│ │ │ ├── CreateTraktListStore.kt
│ │ │ ├── DefaultTraktListDao.kt
│ │ │ ├── DefaultTraktListRepository.kt
│ │ │ ├── DefaultTraktListShowDao.kt
│ │ │ └── TraktListsStore.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── traktlists/
│ │ └── testing/
│ │ └── FakeTraktListRepository.kt
│ ├── trendingshows/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── discover/
│ │ │ └── api/
│ │ │ ├── TrendingShowsDao.kt
│ │ │ ├── TrendingShowsInteractor.kt
│ │ │ ├── TrendingShowsParams.kt
│ │ │ └── TrendingShowsRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── discover/
│ │ │ │ └── implementation/
│ │ │ │ ├── DefaultTrendingShowsDao.kt
│ │ │ │ ├── DefaultTrendingShowsRepository.kt
│ │ │ │ ├── TrendingShowWithImages.kt
│ │ │ │ └── TrendingShowsStore.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── discover/
│ │ │ └── implementation/
│ │ │ └── DefaultTrendingShowsDaoTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── trendingshows/
│ │ └── testing/
│ │ └── FakeTrendingShowsRepository.kt
│ ├── upcomingshows/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── upcomingshows/
│ │ │ └── api/
│ │ │ ├── UpcomingShowsDao.kt
│ │ │ ├── UpcomingShowsInteractor.kt
│ │ │ └── UpcomingShowsRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── data/
│ │ │ │ └── upcomingshows/
│ │ │ │ └── implementation/
│ │ │ │ ├── DefaultUpcomingShowsDao.kt
│ │ │ │ ├── DefaultUpcomingShowsRepository.kt
│ │ │ │ ├── UpcomingShowsStore.kt
│ │ │ │ └── model/
│ │ │ │ ├── UpcomingParams.kt
│ │ │ │ └── UpcomingShowResult.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── upcomingshows/
│ │ │ └── implementation/
│ │ │ └── DefaultUpcomingShowsDaoTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── upcomingshows/
│ │ └── testing/
│ │ └── FakeUpcomingShowsRepository.kt
│ ├── upnext/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── upnext/
│ │ │ └── api/
│ │ │ ├── UpNextDao.kt
│ │ │ ├── UpNextRepository.kt
│ │ │ └── model/
│ │ │ └── NextEpisodeWithShow.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── upnext/
│ │ │ │ └── implementation/
│ │ │ │ ├── DefaultUpNextDao.kt
│ │ │ │ ├── DefaultUpNextRepository.kt
│ │ │ │ └── ShowUpNextStore.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── upnext/
│ │ │ └── implementation/
│ │ │ ├── DefaultUpNextDaoTest.kt
│ │ │ └── DefaultUpNextRepositoryTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── upnext/
│ │ └── testing/
│ │ ├── FakeUpNextDao.kt
│ │ └── FakeUpNextRepository.kt
│ ├── user/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── user/
│ │ │ └── api/
│ │ │ ├── UserDao.kt
│ │ │ ├── UserRepository.kt
│ │ │ ├── UserStatsDao.kt
│ │ │ └── model/
│ │ │ ├── UserProfile.kt
│ │ │ ├── UserProfileStats.kt
│ │ │ └── UserWatchTime.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── data/
│ │ │ │ └── user/
│ │ │ │ └── implementation/
│ │ │ │ ├── DefaultUserDao.kt
│ │ │ │ ├── DefaultUserRepository.kt
│ │ │ │ ├── DefaultUserStatsDao.kt
│ │ │ │ ├── UserStatsStore.kt
│ │ │ │ └── UserStore.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── data/
│ │ │ └── user/
│ │ │ └── implementation/
│ │ │ ├── DefaultUserDaoTest.kt
│ │ │ └── DefaultUserStatsDaoTest.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── user/
│ │ └── testing/
│ │ └── FakeUserRepository.kt
│ ├── watchlist/
│ │ ├── api/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── shows/
│ │ │ └── api/
│ │ │ ├── WatchlistDao.kt
│ │ │ └── WatchlistRepository.kt
│ │ ├── implementation/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── watchlist/
│ │ │ └── implementation/
│ │ │ ├── DefaultWatchlistDao.kt
│ │ │ └── DefaultWatchlistRepository.kt
│ │ └── testing/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── watchlist/
│ │ └── testing/
│ │ └── FakeWatchlistRepository.kt
│ └── watchproviders/
│ ├── api/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── watchproviders/
│ │ └── api/
│ │ ├── WatchProviderDao.kt
│ │ └── WatchProviderRepository.kt
│ ├── implementation/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── data/
│ │ └── watchproviders/
│ │ └── implementation/
│ │ ├── DefaultWatchProviderDao.kt
│ │ ├── DefaultWatchProviderRepository.kt
│ │ └── WatchProvidersStore.kt
│ └── testing/
│ ├── build.gradle.kts
│ └── src/
│ └── commonMain/
│ └── kotlin/
│ └── com/
│ └── thomaskioko/
│ └── tvmaniac/
│ └── data/
│ └── watchproviders/
│ └── testing/
│ └── FakeWatchProviderRepository.kt
├── docs/
│ ├── architecture/
│ │ ├── README.md
│ │ ├── data-layer.md
│ │ ├── dependency-injection.md
│ │ ├── integration-testing.md
│ │ ├── journey-tests.md
│ │ ├── modularization.md
│ │ ├── navigation-codegen.md
│ │ ├── navigation.md
│ │ ├── presentation-layer.md
│ │ └── scopes.md
│ ├── privacy_policy.md
│ ├── setup.md
│ └── terms_conditions.md
├── domain/
│ ├── calendar/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── domain/
│ │ │ └── calendar/
│ │ │ ├── CalendarEpisodeFormatter.kt
│ │ │ ├── CalendarWeekCalculator.kt
│ │ │ ├── FetchCalendarInteractor.kt
│ │ │ ├── ObserveCalendarInteractor.kt
│ │ │ └── model/
│ │ │ ├── DateLabel.kt
│ │ │ ├── GroupedCalendarEntry.kt
│ │ │ └── GroupedEpisodeEntry.kt
│ │ └── commonTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── calendar/
│ │ ├── CalendarWeekCalculatorTest.kt
│ │ └── ObserveCalendarInteractorTest.kt
│ ├── discover/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── domain/
│ │ │ └── discover/
│ │ │ └── DiscoverShowsInteractor.kt
│ │ └── commonTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── discover/
│ │ └── DiscoverShowsInteractorTest.kt
│ ├── episode/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── domain/
│ │ │ └── episode/
│ │ │ ├── MarkEpisodeUnwatchedInteractor.kt
│ │ │ ├── MarkEpisodeWatchedInteractor.kt
│ │ │ ├── ObserveEpisodeByIdInteractor.kt
│ │ │ └── ObserveShowWatchProgressInteractor.kt
│ │ └── commonTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── episode/
│ │ └── MarkEpisodeWatchedInteractorTest.kt
│ ├── followedshows/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── followedshows/
│ │ └── UnfollowShowInteractor.kt
│ ├── genre/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── genre/
│ │ ├── FetchGenreContentInteractor.kt
│ │ └── GenreShowsInteractor.kt
│ ├── library/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── library/
│ │ ├── LibrarySyncWorker.kt
│ │ ├── ObserveLibraryInteractor.kt
│ │ ├── SyncLibraryInteractor.kt
│ │ ├── SyncTasksInitializer.kt
│ │ └── di/
│ │ └── SyncTasksInitializerBindingContainer.kt
│ ├── logout/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── logout/
│ │ └── LogoutInteractor.kt
│ ├── notifications/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── domain/
│ │ │ └── notifications/
│ │ │ ├── EpisodeNotificationWorker.kt
│ │ │ ├── NotificationTasksInitializer.kt
│ │ │ ├── di/
│ │ │ │ └── NotificationTasksInitializerBindingContainer.kt
│ │ │ └── interactor/
│ │ │ ├── RefreshUpcomingSeasonDetailsInteractor.kt
│ │ │ ├── ScheduleDebugEpisodeNotificationInteractor.kt
│ │ │ ├── ScheduleEpisodeNotificationsInteractor.kt
│ │ │ ├── SyncTraktCalendarInteractor.kt
│ │ │ └── ToggleEpisodeNotificationsInteractor.kt
│ │ └── commonTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── notifications/
│ │ └── interactor/
│ │ └── ScheduleEpisodeNotificationsInteractorTest.kt
│ ├── recommendedshows/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── recommendedshows/
│ │ └── RecommendedShowsInteractor.kt
│ ├── seasondetails/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── seasondetails/
│ │ ├── FetchPreviousSeasonsInteractor.kt
│ │ ├── MarkSeasonUnwatchedInteractor.kt
│ │ ├── MarkSeasonWatchedInteractor.kt
│ │ ├── ObservableSeasonDetailsInteractor.kt
│ │ ├── ObserveSeasonWatchProgressInteractor.kt
│ │ ├── ObserveUnwatchedInPreviousSeasonsInteractor.kt
│ │ ├── SeasonDetailsInteractor.kt
│ │ └── model/
│ │ ├── SeasonCast.kt
│ │ ├── SeasonDetailsResult.kt
│ │ └── SeasonImages.kt
│ ├── settings/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── settings/
│ │ └── ObserveSettingsPreferencesInteractor.kt
│ ├── showdetails/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── showdetails/
│ │ ├── FollowShowInteractor.kt
│ │ ├── Mapper.kt
│ │ ├── ObservableShowDetailsInteractor.kt
│ │ ├── ShowContentSyncInteractor.kt
│ │ ├── ShowDetailsInteractor.kt
│ │ └── model/
│ │ └── ShowDetails.kt
│ ├── similarshows/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── similarshows/
│ │ └── SimilarShowsInteractor.kt
│ ├── theme/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── theme/
│ │ ├── ImageQuality.kt
│ │ └── Theme.kt
│ ├── traktlists/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── traktlists/
│ │ ├── CreateTraktListInteractor.kt
│ │ ├── ObserveTraktListsInteractor.kt
│ │ ├── SyncTraktListsInteractor.kt
│ │ └── ToggleShowInListInteractor.kt
│ ├── upnext/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── domain/
│ │ │ └── upnext/
│ │ │ ├── ObserveUpNextInteractor.kt
│ │ │ ├── RefreshUpNextInteractor.kt
│ │ │ ├── UpNextSyncWorker.kt
│ │ │ ├── UpNextTasksInitializer.kt
│ │ │ ├── di/
│ │ │ │ └── UpNextTasksInitializerBindingContainer.kt
│ │ │ └── model/
│ │ │ ├── UpNextResult.kt
│ │ │ └── UpNextSortOption.kt
│ │ └── commonTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── upnext/
│ │ └── ObserveUpNextInteractorTest.kt
│ ├── user/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── user/
│ │ ├── ObserveUserProfileInteractor.kt
│ │ ├── UpdateUserProfileData.kt
│ │ └── model/
│ │ ├── UserProfile.kt
│ │ └── UserStats.kt
│ ├── watchlist/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── domain/
│ │ │ └── watchlist/
│ │ │ ├── ObservableWatchlistInteractor.kt
│ │ │ ├── ObserveUpNextSectionsInteractor.kt
│ │ │ ├── ObserveWatchlistSectionsInteractor.kt
│ │ │ ├── UpNextSectionsMapper.kt
│ │ │ ├── WatchlistSyncInteractor.kt
│ │ │ └── model/
│ │ │ ├── EpisodeBadge.kt
│ │ │ ├── UpNextSections.kt
│ │ │ └── WatchlistSections.kt
│ │ └── commonTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── domain/
│ │ └── watchlist/
│ │ ├── ObservableWatchlistInteractorTest.kt
│ │ ├── ObserveUpNextSectionsInteractorTest.kt
│ │ ├── ObserveWatchlistSectionsInteractorTest.kt
│ │ └── UpNextSectionsMapperTest.kt
│ └── watchproviders/
│ ├── build.gradle.kts
│ └── src/
│ └── commonMain/
│ └── kotlin/
│ └── com/
│ └── thomaskioko/
│ └── tvmaniac/
│ └── domain/
│ └── watchproviders/
│ └── WatchProvidersInteractor.kt
├── fastlane/
│ ├── Appfile
│ ├── Fastfile
│ ├── Matchfile
│ ├── Pluginfile
│ └── README.md
├── features/
│ ├── calendar/
│ │ ├── nav/
│ │ │ └── build.gradle.kts
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── presentation/
│ │ │ │ └── calendar/
│ │ │ │ ├── CalendarAction.kt
│ │ │ │ ├── CalendarPresenter.kt
│ │ │ │ ├── CalendarState.kt
│ │ │ │ ├── CalendarStateMapper.kt
│ │ │ │ └── model/
│ │ │ │ ├── CalendarDateGroup.kt
│ │ │ │ └── CalendarEpisodeItem.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── presentation/
│ │ │ └── calendar/
│ │ │ └── CalendarPresenterTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── main/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── ui/
│ │ │ └── calendar/
│ │ │ └── CalendarScreen.kt
│ │ └── test/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── ui/
│ │ └── calendar/
│ │ └── roborrazi/
│ │ └── CalendarScreenshotTest.kt
│ ├── debug/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── debug/
│ │ │ └── nav/
│ │ │ └── DebugRoute.kt
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── debug/
│ │ │ │ └── presenter/
│ │ │ │ ├── DebugActions.kt
│ │ │ │ ├── DebugPresenter.kt
│ │ │ │ └── DebugState.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── debug/
│ │ │ └── presenter/
│ │ │ └── DebugPresenterTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── debug/
│ │ └── ui/
│ │ └── DebugMenuScreen.kt
│ ├── discover/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── discover/
│ │ │ └── nav/
│ │ │ └── DiscoverNavigator.kt
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── discover/
│ │ │ │ └── presenter/
│ │ │ │ ├── DiscoverShowsAction.kt
│ │ │ │ ├── DiscoverShowsMapper.kt
│ │ │ │ ├── DiscoverShowsPresenter.kt
│ │ │ │ ├── DiscoverViewState.kt
│ │ │ │ ├── di/
│ │ │ │ │ └── DefaultDiscoverNavigator.kt
│ │ │ │ └── model/
│ │ │ │ ├── DiscoverShow.kt
│ │ │ │ └── NextEpisodeUiModel.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── discover/
│ │ │ └── presenter/
│ │ │ └── DiscoverShowsPresenterTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ ├── lint-baseline.xml
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── discover/
│ │ │ └── ui/
│ │ │ ├── DiscoverPreviewParameterProvider.kt
│ │ │ ├── DiscoverScreen.kt
│ │ │ └── component/
│ │ │ ├── CircularIndicator.kt
│ │ │ ├── DiscoverHeaderContent.kt
│ │ │ ├── HorizontalRowContent.kt
│ │ │ ├── NextEpisodeCard.kt
│ │ │ └── NextEpisodesSection.kt
│ │ └── test/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── discover/
│ │ └── roborrazi/
│ │ └── DiscoverScreenshotTest.kt
│ ├── episode-sheet/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── espisodedetails/
│ │ │ └── nav/
│ │ │ └── model/
│ │ │ ├── EpisodeSheetConfig.kt
│ │ │ ├── ScreenSource.kt
│ │ │ └── SheetNavigatorExt.kt
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── presentation/
│ │ │ │ └── episodedetail/
│ │ │ │ ├── EpisodeSheetAction.kt
│ │ │ │ ├── EpisodeSheetMapper.kt
│ │ │ │ ├── EpisodeSheetPresenter.kt
│ │ │ │ └── EpisodeSheetState.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── presentation/
│ │ │ └── episodedetail/
│ │ │ └── EpisodeSheetPresenterTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── episodedetail/
│ │ │ └── ui/
│ │ │ ├── EpisodeDetailBottomSheet.kt
│ │ │ └── EpisodeSheet.kt
│ │ └── test/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── episodedetail/
│ │ └── roborrazi/
│ │ └── EpisodeSheetScreenshotTest.kt
│ ├── genre-shows/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── genreshows/
│ │ │ └── nav/
│ │ │ ├── GenreShowsDestination.kt
│ │ │ └── GenreShowsRoute.kt
│ │ └── presenter/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── genreshows/
│ │ └── presenter/
│ │ └── di/
│ │ └── GenreShowsNavDestinationBinding.kt
│ ├── home/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── home/
│ │ │ └── nav/
│ │ │ ├── HomeRoute.kt
│ │ │ ├── HomeTabNavigator.kt
│ │ │ ├── TabChild.kt
│ │ │ ├── TabDestination.kt
│ │ │ └── di/
│ │ │ ├── TabDestinationMultibindings.kt
│ │ │ └── model/
│ │ │ └── HomeConfig.kt
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── presenter/
│ │ │ │ └── home/
│ │ │ │ ├── HomePresenter.kt
│ │ │ │ └── di/
│ │ │ │ └── DefaultHomeTabNavigator.kt
│ │ │ ├── commonTest/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── presenter/
│ │ │ │ └── home/
│ │ │ │ └── HomePresenterTest.kt
│ │ │ ├── iosTest/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── presenter/
│ │ │ │ └── home/
│ │ │ │ └── HomePresenterIosTest.kt
│ │ │ └── jvmTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── presenter/
│ │ │ └── home/
│ │ │ └── HomePresenterJvmTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ └── java/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── home/
│ │ └── ui/
│ │ └── HomeScreen.kt
│ ├── library/
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── presentation/
│ │ │ └── library/
│ │ │ ├── LibraryAction.kt
│ │ │ ├── LibraryPresenter.kt
│ │ │ ├── LibraryState.kt
│ │ │ └── model/
│ │ │ ├── LibraryShowItem.kt
│ │ │ ├── LibrarySortOption.kt
│ │ │ └── ShowStatus.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── ui/
│ │ └── library/
│ │ ├── LibraryListItem.kt
│ │ ├── LibraryScreen.kt
│ │ ├── LibrarySearchbar.kt
│ │ ├── SortOptionsContent.kt
│ │ └── preview/
│ │ ├── LibraryListItemPreviewParameterProvider.kt
│ │ └── LibraryStatePreviewParameterProvider.kt
│ ├── more-shows/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── moreshows/
│ │ │ └── nav/
│ │ │ └── MoreShowsRoute.kt
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── moreshows/
│ │ │ └── presentation/
│ │ │ ├── MoreShowsAction.kt
│ │ │ ├── MoreShowsPresenter.kt
│ │ │ ├── MoreShowsState.kt
│ │ │ └── TvShow.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── main/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── moreshows/
│ │ │ └── ui/
│ │ │ ├── MoreShowsPreviewParameterProvider.kt
│ │ │ └── MoreShowsScreen.kt
│ │ └── test/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── moreshows/
│ │ └── roborrazi/
│ │ └── MoreShowsScreenTest.kt
│ ├── profile/
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── profile/
│ │ │ │ └── presenter/
│ │ │ │ ├── ProfileAction.kt
│ │ │ │ ├── ProfilePresenter.kt
│ │ │ │ └── model/
│ │ │ │ ├── ProfileInfo.kt
│ │ │ │ ├── ProfileState.kt
│ │ │ │ └── ProfileStats.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── presenter/
│ │ │ └── profile/
│ │ │ └── ProfilePresenterTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── main/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── profile/
│ │ │ └── ui/
│ │ │ ├── ProfilePreviewParameterProvider.kt
│ │ │ ├── ProfileScreen.kt
│ │ │ ├── StatsCardItem.kt
│ │ │ └── UnauthenticatedContent.kt
│ │ └── test/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── profile/
│ │ └── roborazzi/
│ │ └── ProfileScreenTest.kt
│ ├── progress/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── progress/
│ │ │ └── nav/
│ │ │ └── scope/
│ │ │ └── ProgressChildScope.kt
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── presentation/
│ │ │ └── progress/
│ │ │ ├── ProgressAction.kt
│ │ │ ├── ProgressChildGraph.kt
│ │ │ ├── ProgressPresenter.kt
│ │ │ └── ProgressState.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── main/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── ui/
│ │ │ └── progress/
│ │ │ └── ProgressScreen.kt
│ │ └── test/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── ui/
│ │ └── progress/
│ │ └── roborrazi/
│ │ └── ProgressScreenshotTest.kt
│ ├── root/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── root/
│ │ │ ├── model/
│ │ │ │ ├── DeepLinkDestination.kt
│ │ │ │ ├── NotificationPermissionState.kt
│ │ │ │ └── ThemeState.kt
│ │ │ └── nav/
│ │ │ └── NotificationRationale.kt
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── presenter/
│ │ │ │ └── root/
│ │ │ │ ├── DefaultRootPresenter.kt
│ │ │ │ ├── RootPresenter.kt
│ │ │ │ └── di/
│ │ │ │ ├── DefaultNotificationRationale.kt
│ │ │ │ ├── DefaultSheetNavigator.kt
│ │ │ │ └── RootPresenterBindingContainer.kt
│ │ │ ├── commonTest/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── presenter/
│ │ │ │ └── root/
│ │ │ │ └── DefaultRootPresenterTest.kt
│ │ │ ├── iosTest/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── presenter/
│ │ │ │ └── root/
│ │ │ │ └── DefaultRootPresenterIosTest.kt
│ │ │ └── jvmTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── presenter/
│ │ │ └── root/
│ │ │ └── DefaultRootPresenterJvmTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── app/
│ │ └── ui/
│ │ └── RootScreen.kt
│ ├── search/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── search/
│ │ │ └── nav/
│ │ │ └── SearchRoute.kt
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── search/
│ │ │ │ └── presenter/
│ │ │ │ ├── Mapper.kt
│ │ │ │ ├── SearchShowAction.kt
│ │ │ │ ├── SearchShowState.kt
│ │ │ │ ├── SearchShowsPresenter.kt
│ │ │ │ └── model/
│ │ │ │ ├── CategoryItem.kt
│ │ │ │ ├── GenreRowModel.kt
│ │ │ │ ├── ShowGenre.kt
│ │ │ │ └── ShowItem.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── presenter/
│ │ │ └── search/
│ │ │ └── SearchShowsPresenterTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── main/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── search/
│ │ │ └── ui/
│ │ │ ├── SearchPreviewParameterProvider.kt
│ │ │ ├── SearchScreen.kt
│ │ │ └── components/
│ │ │ ├── HorizontalShowContentRow.kt
│ │ │ └── SearchResultItem.kt
│ │ └── test/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── search/
│ │ └── roborrazi/
│ │ └── SearchScreenTest.kt
│ ├── season-details/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── seasondetails/
│ │ │ └── nav/
│ │ │ ├── SeasonDetailsRoute.kt
│ │ │ └── SeasonDetailsUiParam.kt
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── seasondetails/
│ │ │ │ └── presenter/
│ │ │ │ ├── Mapper.kt
│ │ │ │ ├── SeasonDetailsAction.kt
│ │ │ │ ├── SeasonDetailsModel.kt
│ │ │ │ ├── SeasonDetailsPresenter.kt
│ │ │ │ └── model/
│ │ │ │ ├── Cast.kt
│ │ │ │ ├── EpisodeDetailsModel.kt
│ │ │ │ └── SeasonImagesModel.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── seasondetails/
│ │ │ └── presenter/
│ │ │ ├── SeasonPresenterTest.kt
│ │ │ └── data/
│ │ │ └── MockData.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── seasondetails/
│ │ │ └── ui/
│ │ │ ├── SeasonDetailsScreen.kt
│ │ │ ├── SeasonPreviewParameterProvider.kt
│ │ │ └── components/
│ │ │ ├── CollapsableContent.kt
│ │ │ └── EpisodeItem.kt
│ │ └── test/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── seasondetails/
│ │ └── roborrazi/
│ │ └── SeasonScreenshotTest.kt
│ ├── settings/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── settings/
│ │ │ └── nav/
│ │ │ └── SettingsRoute.kt
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── settings/
│ │ │ │ └── presenter/
│ │ │ │ ├── SettingsActions.kt
│ │ │ │ ├── SettingsPresenter.kt
│ │ │ │ ├── SettingsState.kt
│ │ │ │ └── ThemeModel.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── presenter/
│ │ │ └── settings/
│ │ │ └── SettingsPresenterTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── main/
│ │ │ ├── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── settings/
│ │ │ │ └── ui/
│ │ │ │ ├── AboutSheetContent.kt
│ │ │ │ ├── SettingsPreviewParameterProvider.kt
│ │ │ │ ├── SettingsScreen.kt
│ │ │ │ ├── ThemePreviewSwatch.kt
│ │ │ │ └── ThemeSelectorSection.kt
│ │ │ └── res/
│ │ │ └── drawable/
│ │ │ └── ic_app_launcher.xml
│ │ └── test/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── seasondetails/
│ │ └── roborrazi/
│ │ └── SettingsScreenshotTest.kt
│ ├── show-details/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── showdetails/
│ │ │ └── nav/
│ │ │ ├── ShowDetailsRoute.kt
│ │ │ └── model/
│ │ │ ├── ShowDetailsParam.kt
│ │ │ └── ShowSeasonDetailsParam.kt
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── presenter/
│ │ │ │ └── showdetails/
│ │ │ │ ├── ShowDetailsAction.kt
│ │ │ │ ├── ShowDetailsContent.kt
│ │ │ │ ├── ShowDetailsMapper.kt
│ │ │ │ ├── ShowDetailsPresenter.kt
│ │ │ │ └── model/
│ │ │ │ ├── CastModel.kt
│ │ │ │ ├── ContinueTrackingEpisodeModel.kt
│ │ │ │ ├── ProviderModel.kt
│ │ │ │ ├── SeasonModel.kt
│ │ │ │ ├── ShowDetailsModel.kt
│ │ │ │ ├── ShowModel.kt
│ │ │ │ ├── TrailerModel.kt
│ │ │ │ └── TraktListModel.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── presenter/
│ │ │ └── showdetails/
│ │ │ ├── MockData.kt
│ │ │ └── ShowDetailsPresenterTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── main/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── showdetails/
│ │ │ └── ui/
│ │ │ ├── DetailPreviewParameterProvider.kt
│ │ │ ├── ShowDetailScreen.kt
│ │ │ └── components/
│ │ │ ├── ContinueTrackingCard.kt
│ │ │ ├── ContinueTrackingSection.kt
│ │ │ ├── SeasonChipItem.kt
│ │ │ ├── ShowListSheetContent.kt
│ │ │ └── WatchProgressSection.kt
│ │ └── test/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── showdetails/
│ │ └── roborrazi/
│ │ ├── ShowDetailsScreenScreenshotTest.kt
│ │ └── ShowListSheetScreenshotTest.kt
│ ├── trailers/
│ │ ├── nav/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── trailers/
│ │ │ └── nav/
│ │ │ └── TrailersRoute.kt
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── presenter/
│ │ │ │ └── trailers/
│ │ │ │ ├── Mapper.kt
│ │ │ │ ├── TrailersAction.kt
│ │ │ │ ├── TrailersPresenter.kt
│ │ │ │ ├── TrailersState.kt
│ │ │ │ └── model/
│ │ │ │ └── Trailer.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── presenter/
│ │ │ └── trailers/
│ │ │ └── TrailersPresenterTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── trailers/
│ │ └── ui/
│ │ ├── TrailerPreviewParameterProvider.kt
│ │ └── TrailersScreen.kt
│ ├── upnext/
│ │ ├── presenter/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ ├── commonMain/
│ │ │ │ └── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── thomaskioko/
│ │ │ │ └── tvmaniac/
│ │ │ │ └── presentation/
│ │ │ │ └── upnext/
│ │ │ │ ├── UpNextAction.kt
│ │ │ │ ├── UpNextPresenter.kt
│ │ │ │ ├── UpNextState.kt
│ │ │ │ └── model/
│ │ │ │ └── UpNextEpisodeUiModel.kt
│ │ │ └── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── presentation/
│ │ │ └── upnext/
│ │ │ └── UpNextPresenterTest.kt
│ │ └── ui/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── main/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── ui/
│ │ │ └── upnext/
│ │ │ ├── UpNextListItem.kt
│ │ │ ├── UpNextScreen.kt
│ │ │ └── preview/
│ │ │ └── UpNextPreviewParameterProvider.kt
│ │ └── test/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── ui/
│ │ └── upnext/
│ │ └── roborrazi/
│ │ └── UpNextScreenshotTest.kt
│ └── watchlist/
│ ├── presenter/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── watchlist/
│ │ │ └── presenter/
│ │ │ ├── Mapper.kt
│ │ │ ├── WatchlistAction.kt
│ │ │ ├── WatchlistPresenter.kt
│ │ │ ├── WatchlistState.kt
│ │ │ └── model/
│ │ │ ├── EpisodeBadge.kt
│ │ │ ├── NextEpisodeItem.kt
│ │ │ ├── SectionedEpisodes.kt
│ │ │ ├── SectionedItems.kt
│ │ │ ├── UpNextEpisodeItem.kt
│ │ │ └── WatchlistItem.kt
│ │ └── commonTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ ├── domain/
│ │ │ └── watchlist/
│ │ │ ├── MockData.kt
│ │ │ └── WatchlistPresenterTest.kt
│ │ └── watchlist/
│ │ └── presenter/
│ │ └── FakeWatchlistPresenterBuilder.kt
│ └── ui/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── ui/
│ │ └── library/
│ │ ├── WatchListUpNextListItem.kt
│ │ ├── WatchlistListItem.kt
│ │ ├── WatchlistPreviewParameterProvider.kt
│ │ ├── WatchlistScreen.kt
│ │ └── component/
│ │ └── Searchbar.kt
│ └── test/
│ └── kotlin/
│ └── com/
│ └── thomaskioko/
│ └── tvmaniac/
│ └── watchlist/
│ └── roborrazi/
│ └── WatchlistScreenTest.kt
├── gradle/
│ ├── gradle-daemon-jvm.properties
│ ├── libs.versions.toml
│ ├── lint.xml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── i18n/
│ ├── api/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── i18n/
│ │ └── api/
│ │ └── Localizer.kt
│ ├── generator/
│ │ ├── build.gradle.kts
│ │ ├── lint-baseline.xml
│ │ └── src/
│ │ ├── androidMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── i18n/
│ │ │ └── Resources.kt
│ │ ├── commonMain/
│ │ │ └── moko-resources/
│ │ │ ├── base/
│ │ │ │ ├── plurals.xml
│ │ │ │ └── strings.xml
│ │ │ ├── de/
│ │ │ │ ├── plurals.xml
│ │ │ │ └── strings.xml
│ │ │ └── fr/
│ │ │ ├── plurals.xml
│ │ │ └── strings.xml
│ │ ├── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── i18n/
│ │ │ └── generator/
│ │ │ └── ResourceTest.kt
│ │ └── iosMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── i18n/
│ │ └── Resources.kt
│ ├── implementation/
│ │ ├── build.gradle.kts
│ │ ├── lint-baseline.xml
│ │ └── src/
│ │ ├── androidHostTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── i18n/
│ │ │ └── util/
│ │ │ └── BaseResourceTests.kt
│ │ ├── androidMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── i18n/
│ │ │ └── PlatformLocalizer.android.kt
│ │ ├── commonMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── i18n/
│ │ │ ├── LocalizedErrorToStringMapper.kt
│ │ │ ├── MokoLocaleInitializer.kt
│ │ │ ├── MokoResourcesLocalizer.kt
│ │ │ ├── PlatformLocalizer.kt
│ │ │ └── di/
│ │ │ └── MokoLocaleInitializerBindingContainer.kt
│ │ ├── commonTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── i18n/
│ │ │ ├── LocalizedStringTest.kt
│ │ │ ├── MokoLocalizerTest.kt
│ │ │ └── util/
│ │ │ └── BaseResourceTests.kt
│ │ ├── iosMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── i18n/
│ │ │ └── PlatformLocalizer.ios.kt
│ │ ├── iosTest/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── i18n/
│ │ │ └── util/
│ │ │ └── BaseResourceTests.kt
│ │ ├── jvmMain/
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── thomaskioko/
│ │ │ └── tvmaniac/
│ │ │ └── i18n/
│ │ │ └── PlatformLocalizer.jvm.kt
│ │ └── jvmTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── i18n/
│ │ └── util/
│ │ └── BaseResourceTests.kt
│ └── testing/
│ ├── build.gradle.kts
│ └── src/
│ ├── androidMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── i18n/
│ │ └── testing/
│ │ └── util/
│ │ ├── BaseLocalizerTest.android.kt
│ │ └── StringDescExt.kt
│ ├── commonMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── i18n/
│ │ └── testing/
│ │ ├── FakeLocalizer.kt
│ │ └── util/
│ │ ├── BaseLocalizerTest.kt
│ │ └── StringDescExt.kt
│ ├── iosMain/
│ │ └── kotlin/
│ │ └── com/
│ │ └── thomaskioko/
│ │ └── tvmaniac/
│ │ └── i18n/
│ │ └── testing/
│ │ └── util/
│ │ ├── BaseLocalizerTest.ios.kt
│ │ └── StringDescExt.ios.kt
│ └── jvmMain/
│ └── kotlin/
│ └── com/
│ └── thomaskioko/
│ └── tvmaniac/
│ └── i18n/
│ └── testing/
│ └── util/
│ ├── BaseLocalizerTest.jvm.kt
│ └── StringDescExt.jvm.kt
├── ios/
│ ├── .gitignore
│ ├── .swiftformat
│ ├── Config/
│ │ ├── Debug.xcconfig
│ │ └── Release.xcconfig
│ ├── Modules/
│ │ ├── CoreKit/
│ │ │ ├── Package.swift
│ │ │ └── Sources/
│ │ │ └── CoreKit/
│ │ │ ├── CoreLogger.swift
│ │ │ ├── DefaultDiagnosticLogger.swift
│ │ │ ├── DiagnosticLogger.swift
│ │ │ ├── FirebaseCrashlyticsBridge.swift
│ │ │ ├── ImageCacheManager.swift
│ │ │ ├── MemoryMonitor.swift
│ │ │ └── SystemMemory.swift
│ │ ├── SnapshotTestingLib/
│ │ │ ├── .gitignore
│ │ │ ├── .swiftpm/
│ │ │ │ └── xcode/
│ │ │ │ └── package.xcworkspace/
│ │ │ │ └── xcshareddata/
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ ├── Package.swift
│ │ │ └── Sources/
│ │ │ └── SnapshotTestingLib/
│ │ │ └── SnapshotTesting+Extensions.swift
│ │ ├── SwiftUIComponents/
│ │ │ ├── .gitignore
│ │ │ ├── .swiftpm/
│ │ │ │ └── xcode/
│ │ │ │ └── package.xcworkspace/
│ │ │ │ └── xcshareddata/
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ ├── Package.swift
│ │ │ ├── Sources/
│ │ │ │ └── SwiftUIComponents/
│ │ │ │ ├── Components/
│ │ │ │ │ ├── BorderTextView.swift
│ │ │ │ │ ├── BottomSheet/
│ │ │ │ │ │ └── EpisodeDetailSheetContent.swift
│ │ │ │ │ ├── Buttons/
│ │ │ │ │ │ ├── CircularButton.swift
│ │ │ │ │ │ ├── FilledImageButton.swift
│ │ │ │ │ │ ├── OutlinedButton.swift
│ │ │ │ │ │ ├── RoundedButton.swift
│ │ │ │ │ │ └── TvManiacButton.swift
│ │ │ │ │ ├── CarouselView.swift
│ │ │ │ │ ├── CastListView.swift
│ │ │ │ │ ├── ChevronTitle.swift
│ │ │ │ │ ├── ChipView.swift
│ │ │ │ │ ├── CircularIndicator.swift
│ │ │ │ │ ├── ContinueTracking/
│ │ │ │ │ │ ├── ContinueTrackingCard.swift
│ │ │ │ │ │ ├── ContinueTrackingSection.swift
│ │ │ │ │ │ └── SwiftContinueTrackingEpisode.swift
│ │ │ │ │ ├── EmptyUIView.swift
│ │ │ │ │ ├── Episode/
│ │ │ │ │ │ ├── EpisodeCollapsible.swift
│ │ │ │ │ │ ├── EpisodeItemView.swift
│ │ │ │ │ │ └── EpisodeListView.swift
│ │ │ │ │ ├── FilterChip.swift
│ │ │ │ │ ├── FilterChipSection.swift
│ │ │ │ │ ├── FlowLayout.swift
│ │ │ │ │ ├── FullScreenView.swift
│ │ │ │ │ ├── GlassButton.swift
│ │ │ │ │ ├── GlassToolbar.swift
│ │ │ │ │ ├── GridView.swift
│ │ │ │ │ ├── HorizontalItemListView.swift
│ │ │ │ │ ├── ImageGalleryContentView.swift
│ │ │ │ │ ├── Images/
│ │ │ │ │ │ ├── AvatarView.swift
│ │ │ │ │ │ ├── BackdropPosterCard.swift
│ │ │ │ │ │ ├── CastCardView.swift
│ │ │ │ │ │ ├── FeaturedContentPosterView.swift
│ │ │ │ │ │ ├── HeaderCoverArtWorkView.swift
│ │ │ │ │ │ ├── LazyResizableImage.swift
│ │ │ │ │ │ ├── PosterCardView.swift
│ │ │ │ │ │ ├── PosterItemView.swift
│ │ │ │ │ │ ├── PosterPlaceholder.swift
│ │ │ │ │ │ ├── ProviderItemView.swift
│ │ │ │ │ │ └── TransparentImageBackground.swift
│ │ │ │ │ ├── LibraryListItemView.swift
│ │ │ │ │ ├── LoadingIndicatorView.swift
│ │ │ │ │ ├── Models/
│ │ │ │ │ │ ├── DebugMenuItem.swift
│ │ │ │ │ │ ├── SettingsModels.swift
│ │ │ │ │ │ ├── ShowPosterImage.swift
│ │ │ │ │ │ ├── SwiftCalendarDateGroup.swift
│ │ │ │ │ │ ├── SwiftCast.swift
│ │ │ │ │ │ ├── SwiftGenreRow.swift
│ │ │ │ │ │ ├── SwiftGenres.swift
│ │ │ │ │ │ ├── SwiftLibraryItem.swift
│ │ │ │ │ │ ├── SwiftProfile.swift
│ │ │ │ │ │ ├── SwiftProviders.swift
│ │ │ │ │ │ ├── SwiftSearchShow.swift
│ │ │ │ │ │ ├── SwiftSeason.swift
│ │ │ │ │ │ ├── SwiftShow.swift
│ │ │ │ │ │ ├── SwiftShowGenre.swift
│ │ │ │ │ │ ├── SwiftTrailer.swift
│ │ │ │ │ │ └── SwiftTraktListItem.swift
│ │ │ │ │ ├── NavigationTopBar.swift
│ │ │ │ │ ├── NextEpisode/
│ │ │ │ │ │ ├── NextEpisodeCard.swift
│ │ │ │ │ │ ├── NextEpisodesSection.swift
│ │ │ │ │ │ ├── SwiftNextEpisode.swift
│ │ │ │ │ │ └── UpNextListItemView.swift
│ │ │ │ │ ├── NotificationRationaleSheet.swift
│ │ │ │ │ ├── OverviewBoxView.swift
│ │ │ │ │ ├── ParallaxView.swift
│ │ │ │ │ ├── ProviderListView.swift
│ │ │ │ │ ├── ScanlineOverlay.swift
│ │ │ │ │ ├── Search/
│ │ │ │ │ │ ├── SearchItemView.swift
│ │ │ │ │ │ ├── SearchResultListView.swift
│ │ │ │ │ │ └── ShowContent/
│ │ │
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
# noinspection EditorConfigKeyCorrectness
[*.{kt,kts}]
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ktlint_function_naming_ignore_when_annotated_with = Composable, Test
ktlint_standard_backing-property-naming = disabled
ktlint_standard_binary-expression-wrapping = disabled
ktlint_standard_chain-method-continuation = disabled
ktlint_standard_class-signature = disabled
ktlint_standard_condition-wrapping = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-literal = disabled
ktlint_standard_function-type-modifier-spacing = disabled
ktlint_standard_multiline-loop = disabled
ktlint_standard_function-signature = disabled
================================================
FILE: .geminiignore
================================================
.gradle/
build/
.kotlin/
.idea/
.build/
ios/build/
ios/SourcePackages/
derived_data/
*.log
*.class
*.apk
*.ap_
*.dex
local.properties
.DS_Store
fastlane/test_output
fastlane/builds
node_modules/
dist/
target/
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: Report a reproducible bug in TvManiac.
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to file a bug report. Please fill out as much detail as you can. Issues without reproduction steps are hard to act on.
- type: textarea
id: description
attributes:
label: Description
description: What happened, and what did you expect to happen instead?
placeholder: A clear description of the bug.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to Reproduce
description: Minimal, numbered steps that reliably reproduce the issue.
placeholder: |
1. Open the app.
2. Navigate to...
3. Tap...
4. Observe the bug.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
placeholder: What you expected to happen.
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
placeholder: What actually happened. Include error messages if any.
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
options:
- Android
- iOS
- Both
validations:
required: true
- type: input
id: device
attributes:
label: Device and OS Version
description: e.g. Pixel 8 on Android 15, iPhone 15 Pro on iOS 18.2.
placeholder: Device model and OS version.
validations:
required: true
- type: input
id: version
attributes:
label: App Version or Commit SHA
description: Version from the About screen, or the commit SHA if running from source.
placeholder: e.g. 1.4.0, or 3298cb27f.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs, Screenshots, or Recordings
description: Logcat output, Xcode console logs, screenshots, or screen recordings. Redact any API keys or personal data.
render: shell
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I searched existing issues and did not find a duplicate.
required: true
- label: I redacted any secrets, tokens, or personal data from logs and screenshots.
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: Suggest a new feature or improvement for TvManiac.
title: "[Feature]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for suggesting an improvement. Please describe the problem first, then the proposed solution. Ideas grounded in a concrete use case are easier to evaluate.
- type: textarea
id: problem
attributes:
label: Problem Statement
description: What problem are you trying to solve? What is missing or inconvenient today?
placeholder: As a user, I want... so that...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: Describe the behavior you would like to see. Include mockups or references if helpful.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Other approaches you thought about and why they are less suitable.
- type: dropdown
id: platform
attributes:
label: Platform Scope
options:
- Android
- iOS
- Both
- Not sure
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional Context
description: Any other context, links, or references that help motivate the feature.
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I searched existing issues and discussions and did not find a duplicate.
required: true
================================================
FILE: .github/actions/setup-android-release/action.yml
================================================
name: 'Setup Android Release'
description: 'Common setup for Android release builds: Gradle, Ruby, google-services.json, and signing key decryption'
inputs:
google-services-json:
description: 'Base64-encoded google-services.json for release'
required: false
signing-encrypt-key:
description: 'Encryption key for decrypting signing keystores'
required: true
runs:
using: 'composite'
steps:
- name: Setup Gradle Environment
uses: ./.github/actions/setup-gradle
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Install google-services.json
shell: bash
env:
GOOGLE_SERVICES_JSON_RELEASE: ${{ inputs.google-services-json }}
run: |
if [ -n "$GOOGLE_SERVICES_JSON_RELEASE" ]; then
mkdir -p app/src/release
echo "$GOOGLE_SERVICES_JSON_RELEASE" | base64 --decode > app/src/release/google-services.json
else
echo "warning: google-services.json not provided - Firebase will be disabled"
fi
- name: Decrypt signing keys
shell: bash
env:
SIGNING_ENCRYPT_KEY: ${{ inputs.signing-encrypt-key }}
run: |
openssl aes-256-cbc -d -pbkdf2 -in release/app-release.aes -out release/app-release.jks -k "$SIGNING_ENCRYPT_KEY"
openssl aes-256-cbc -d -pbkdf2 -in release/play-service-account.aes -out release/play-service-account.json -k "$SIGNING_ENCRYPT_KEY"
if [ -f "release/firebase-sa.aes" ]; then
openssl aes-256-cbc -d -pbkdf2 -in release/firebase-sa.aes -out release/firebase-sa.json -k "$SIGNING_ENCRYPT_KEY"
fi
================================================
FILE: .github/actions/setup-gradle/action.yml
================================================
name: 'Setup Gradle Environment'
description: 'Common setup for Gradle builds with JDK 21 and environment variables'
inputs:
gradle-cache-disabled:
description: 'Disable Gradle cache (default: false = cache enabled). Only set to true for baseline-profile workflow.'
required: false
default: 'false'
gradle-cache-read-only:
description: 'Make Gradle cache read-only (default: auto-detected from event type). PRs read cache but do not write.'
required: false
default: ${{ github.event_name == 'pull_request' }}
runs:
using: 'composite'
steps:
- name: Set up JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin'
java-version: '21'
- name: Install git-cliff
uses: kenji-miyake/setup-git-cliff@2778609c643a39a2576c4bae2e493b855eb4aee8 # v2.0.1
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
cache-provider: basic
cache-disabled: ${{ inputs.gradle-cache-disabled }}
cache-read-only: ${{ inputs.gradle-cache-read-only }}
================================================
FILE: .github/actions/setup-ios/action.yml
================================================
name: 'Setup iOS Environment'
description: 'Common setup for iOS builds with Xcode, Ruby, SPM cache, and Gradle'
inputs:
xcode-version:
description: 'Xcode version to use'
required: true
spm-cache-path:
description: 'Path to SPM dependencies for caching'
required: false
default: 'ios/SourcePackages'
runs:
using: 'composite'
steps:
- name: 📀 Setup Xcode version
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ inputs.xcode-version }}
- name: 📱 Verify Simulator Availability
shell: bash
run: |
echo "Xcode: $(xcodebuild -version | head -1)"
echo "Available iOS runtimes:"
xcrun simctl list runtimes iOS
echo "Available iPhone simulators:"
xcrun simctl list devices available iPhone
- name: 💎 Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Cache SPM dependencies
uses: actions/cache@v5
with:
path: ${{ inputs.spm-cache-path }}
key: ${{ runner.os }}-spm-${{ github.sha }}
restore-keys: |
${{ runner.os }}-spm-
- name: 🐘 Setup Gradle
uses: ./.github/actions/setup-gradle
================================================
FILE: .github/actions/setup-ios-release/action.yml
================================================
name: 'Setup iOS Release'
description: 'Common setup for iOS release builds: Xcode, Ruby, SPM cache, Gradle, and GoogleService-Info.plist'
inputs:
xcode-version:
description: 'Xcode version to use'
required: true
google-service-info-plist:
description: 'Base64-encoded GoogleService-Info.plist for release'
required: false
runs:
using: 'composite'
steps:
- name: Setup iOS Environment
uses: ./.github/actions/setup-ios
with:
xcode-version: ${{ inputs.xcode-version }}
- name: Install GoogleService-Info.plist
shell: bash
env:
GOOGLE_SERVICE_INFO: ${{ inputs.google-service-info-plist }}
run: |
if [ -n "$GOOGLE_SERVICE_INFO" ]; then
mkdir -p ios/ios/Firebase/Release
echo "$GOOGLE_SERVICE_INFO" | base64 --decode > ios/ios/Firebase/Release/GoogleService-Info.plist
else
echo "warning: GoogleService-Info.plist not provided - Firebase will be disabled"
fi
================================================
FILE: .github/release.yml
================================================
changelog:
exclude:
labels:
- skip-changelog
authors:
- renovate[bot]
- dependabot[bot]
categories:
- title: "Features"
labels:
- feature
- enhancement
- title: "Bug Fixes"
labels:
- bug
- fix
- title: "Dependencies"
labels:
- dependencies
- title: "Other Changes"
exclude:
labels:
- feature
- enhancement
- bug
- fix
- dependencies
- skip-changelog
================================================
FILE: .github/renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":disableRateLimiting",
":semanticCommitsDisabled"
],
"labels": [
"renovate"
],
"ignoreDeps": [
"renovate/renovate"
],
"ignorePaths": [
"**/.ruby-version"
],
"packageRules": [
{
"matchDatasources": [
"maven"
],
"registryUrls": [
"https://repo.maven.apache.org/maven2",
"https://dl.google.com/android/maven2",
"https://plugins.gradle.org/m2"
]
},
{
"groupName": "Compose",
"matchPackageNames": [
"/androidx.compose.runtime/",
"/androidx.compose.ui/",
"/androidx.compose.foundation/",
"/androidx.compose.animation/",
"/androidx.compose.material/",
"/androidx.compose.material3/",
"/org.jetbrains.compose$/",
"/org.jetbrains.compose.runtime/",
"/org.jetbrains.compose.ui/",
"/org.jetbrains.compose.foundation/",
"/org.jetbrains.compose.animation/",
"/org.jetbrains.compose.material/",
"/org.jetbrains.compose.material3/"
]
},
{
"groupName": "Roborazzi",
"matchPackageNames": [
"/io.github.takahirom.roborazzi/"
]
}
],
"dependencyDashboard": true,
"rebaseWhen": "conflicted",
"configMigration": true,
"commitMessagePrefix": "chore(deps):"
}
================================================
FILE: .github/workflows/baseline-profile.yml
================================================
name: Weekly Baseline Profile Generation
on:
schedule:
- cron: '30 0 * * 0,3,5' #Every Sunday, Wednesday, and Friday at 12:30AM
env:
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
TRAKT_CLIENT_SECRET: ${{ secrets.TRAKT_CLIENT_SECRET }}
TRAKT_REDIRECT_URI: ${{ secrets.TRAKT_REDIRECT_URI }}
jobs:
baseline_profiles:
name: "Generate Baseline Profiles"
runs-on: ubuntu-latest
permissions:
contents: write
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Setup Gradle Environment
uses: ./.github/actions/setup-gradle
with:
gradle-cache-disabled: 'true'
- name: Setup Android SDK
uses: android-actions/setup-android@v4
- name: Accept licenses
run: yes | sdkmanager --licenses || trueMovr
- name: Build app and benchmark
run: ./gradlew assembleNonMinifiedRelease -Papp.debugOnly=false
- name: Clear Gradle Managed Devices
run: ./gradlew cleanManagedDevices
- name: Generate Baseline Profile
run: ./gradlew :app:generateBaselineProfile
-Papp.debugOnly=false
-Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=baselineprofile
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
-Pandroid.experimental.androidTest.numManagedDeviceShards=1
--no-configuration-cache
## ToDo: Commit baseline profile changes to main.
================================================
FILE: .github/workflows/beta-release.yml
================================================
name: Beta Release
on:
workflow_dispatch:
inputs:
skip_android:
description: 'Skip Android build'
type: boolean
default: false
skip_ios:
description: 'Skip iOS build'
type: boolean
default: false
permissions:
contents: write
env:
XCODE_VERSION: 16.4
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
TRAKT_CLIENT_SECRET: ${{ secrets.TRAKT_CLIENT_SECRET }}
TRAKT_REDIRECT_URI: ${{ secrets.TRAKT_REDIRECT_URI }}
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.bump.outputs.version }}
build: ${{ steps.bump.outputs.build }}
sha: ${{ steps.push.outputs.sha }}
steps:
- name: Checkout main
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN }}
- name: Setup Gradle Environment
uses: ./.github/actions/setup-gradle
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Bump beta version
id: bump
run: |
./gradlew bumpVersion -Ptype=beta
version=$(grep 'VERSION_NUMBER' version.txt | sed 's/.*= *//')
build=$(grep 'BUILD_NUMBER' version.txt | sed 's/.*= *//')
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "build=$build" >> "$GITHUB_OUTPUT"
echo "Version: $version, BUILD_NUMBER: $build"
- name: Commit and push
id: push
run: |
git add version.txt
git commit -m "chore: bump beta build number to ${{ steps.bump.outputs.build }}"
git push origin main
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
build-android:
needs: prepare
if: ${{ !inputs.skip_android }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.sha }}
- name: Setup Android Release
uses: ./.github/actions/setup-android-release
with:
google-services-json: ${{ secrets.GOOGLE_SERVICES_JSON_RELEASE }}
signing-encrypt-key: ${{ secrets.SIGNING_ENCRYPT_KEY }}
- name: Build release AAB and APK
env:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
run: |
./gradlew :app:bundleRelease :app:assembleRelease \
-Pandroidx.baselineprofile.skipgeneration=true \
-Papp.debugOnly=false \
-Papp.versionSuffix=-beta \
-PreleaseStoreFile=release/app-release.jks \
-PreleaseStorePassword="$SIGNING_STORE_PASSWORD" \
-PreleaseKeyAlias=tvmaniac \
-PreleaseKeyPassword="$SIGNING_KEY_PASSWORD" \
--no-configuration-cache
- name: Print build info
run: |
echo "Version: $(grep 'VERSION_NUMBER' version.txt | sed 's/.*= *//')-beta"
echo "Build: $(grep 'BUILD_NUMBER' version.txt | sed 's/.*= *//')"
- name: Deploy to Play Store open testing track
run: |
export PLAY_STORE_SERVICE_ACCOUNT_JSON=$(cat release/play-service-account.json)
bundle exec fastlane android deploy_play_store track:beta
- name: Distribute to Firebase App Distribution
env:
FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }}
run: |
if [ -n "$FIREBASE_APP_ID" ] && [ -f "release/firebase-sa.json" ]; then
export FIREBASE_APP_DISTRIBUTION_SA=release/firebase-sa.json
bundle exec fastlane android distribute_firebase
else
echo "warning: Firebase not configured — skipping distribution"
fi
build-ios:
needs: prepare
if: ${{ !inputs.skip_ios }}
runs-on: macos-15
timeout-minutes: 60
env:
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 60
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.sha }}
- name: Setup iOS Release
uses: ./.github/actions/setup-ios-release
with:
xcode-version: ${{ env.XCODE_VERSION }}
google-service-info-plist: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_RELEASE }}
- name: Build and upload to TestFlight
env:
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
run: bundle exec fastlane ios build_beta
- name: Upload iOS artifacts
uses: actions/upload-artifact@v7
if: always()
with:
name: ios-internal
path: |
build/
fastlane/logs
================================================
FILE: .github/workflows/ci.yml
================================================
name: build
on:
push:
branches: [ main ]
pull_request:
types: [ opened, synchronize ]
workflow_call:
concurrency:
group: ci-${{ github.ref }}-${{ github.head_ref }}-${{ github.workflow }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
XCODE_VERSION: 16.4
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
TRAKT_CLIENT_SECRET: ${{ secrets.TRAKT_CLIENT_SECRET }}
TRAKT_REDIRECT_URI: ${{ secrets.TRAKT_REDIRECT_URI }}
jobs:
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: 🐘 Setup Gradle Environment
uses: ./.github/actions/setup-gradle
- name: Run Spotless
run: ./gradlew spotlessCheck -Papp.debugOnly=false
- name: Lint Project
run: ./gradlew :app:lintRelease -Papp.debugOnly=false
- name: Upload Lint Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: android-lint-report
path: build/reports/lint/
if-no-files-found: ignore
- name: Dependency Health
run: ./gradlew buildHealth -Papp.debugOnly=false
- name: Upload Dependency Health Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: dependency-health-report
path: '**/build/reports/dependency-analysis/build-health-report.txt'
swift-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: 🔍 Run SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
with:
args: --config .swiftlint.yml
build-android:
needs: [static-analysis]
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: 🚚 Checkout Code
uses: actions/checkout@v6
- name: 🐘 Setup Gradle Environment
uses: ./.github/actions/setup-gradle
- name: 🔥 Install google-services.json
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: |
if [ -n "$GOOGLE_SERVICES_JSON" ]; then
echo "$GOOGLE_SERVICES_JSON" | base64 --decode > app/google-services.json
else
echo "warning: GOOGLE_SERVICES_JSON secret not set — Firebase will be disabled"
fi
- name: 🤖 Build Android App
run: |
./gradlew :app:assembleRelease \
:app:bundleRelease \
-Pandroidx.baselineprofile.skipgeneration=true \
-Pcompose.enableCompilerMetrics=true \
-Pcompose.enableCompilerReports=true \
-Papp.debugOnly=false
- name: Upload Compose Compiler Metrics
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: compose-compiler-metrics
path: |
**/build/compose_metrics/
**/build/compose_reports/
linux-test:
needs: [static-analysis]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: 🐘 Setup Gradle Environment
uses: ./.github/actions/setup-gradle
- name: Run Linux Tests
run: ./gradlew linuxTest -Papp.debugOnly=false
- name: Upload Test Reports
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: linux-test-report
path: '**/build/reports/*'
# ios-test:
# needs: [static-analysis]
# runs-on: macos-15
# environment:
# name: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && 'Protected' || '' }}
# steps:
# - uses: actions/checkout@v6
#
# - name: 🐘 Setup Gradle Environment
# uses: ./.github/actions/setup-gradle
#
# - name: Run iOS Tests
# run: ./gradlew iosTest -Papp.enableIos=true -Papp.debugOnly=false
#
# - name: Upload Test Reports
# uses: actions/upload-artifact@v7
# if: ${{ !cancelled() }}
# with:
# name: ios-test-report
# path: '**/build/reports/*'
build-ios:
needs: [swift-lint]
runs-on: macos-15
timeout-minutes: 60
env:
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 60
steps:
- name: 🚚 Checkout project
uses: actions/checkout@v6
- name: 📱 Setup iOS Environment
uses: ./.github/actions/setup-ios
with:
xcode-version: ${{ env.XCODE_VERSION }}
- name: 🔥 Install GoogleService-Info.plist
env:
GOOGLE_SERVICE_INFO: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST }}
run: |
if [ -n "$GOOGLE_SERVICE_INFO" ]; then
mkdir -p ios/ios/Firebase/Debug
echo "$GOOGLE_SERVICE_INFO" | base64 --decode > ios/ios/Firebase/Debug/GoogleService-Info.plist
else
echo "warning: GOOGLE_SERVICE_INFO_PLIST secret not set — Firebase will be disabled"
fi
- name: 🧱 Build iOS App
run: bundle exec fastlane build_tvmaniac
- name: Upload build artifacts
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: ios-build
path: |
derived_data/Build/Products/
fastlane/logs
ios-snapshot-test:
needs: [swift-lint]
runs-on: macos-15
timeout-minutes: 60
env:
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 60
steps:
- name: 🚚 Checkout project
uses: actions/checkout@v6
- name: 📱 Setup iOS Environment
uses: ./.github/actions/setup-ios
with:
xcode-version: ${{ env.XCODE_VERSION }}
- name: 📸 Run Snapshot Tests
run: bundle exec fastlane snapshot_tests
- name: Upload test results
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: snapshot-test-results
path: |
fastlane/test_output
fastlane/logs
derived_data/Logs/Test
================================================
FILE: .github/workflows/compare-screenshot.yml
================================================
name: Compare Screenshot
on:
pull_request:
permissions: {}
env:
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
TRAKT_CLIENT_SECRET: ${{ secrets.TRAKT_CLIENT_SECRET }}
TRAKT_REDIRECT_URI: ${{ secrets.TRAKT_REDIRECT_URI }}
jobs:
compare-screenshot-test:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: write # for pushing screenshot-diff to companion branch
actions: write # for upload-artifact
pull-requests: write # for creating a comment on pull requests
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Gradle Environment
uses: ./.github/actions/setup-gradle
- name: Get base commit SHA
id: get-base-commit
run: |
git fetch origin ${{ github.event.pull_request.base.ref }}
BASE_SHA=$(git merge-base origin/${{ github.event.pull_request.base.ref }} HEAD)
echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT"
- uses: dawidd6/action-download-artifact@v21
continue-on-error: true
with:
name: screenshot
workflow: store-screenshot.yml
commit: ${{ steps.get-base-commit.outputs.base_sha }}
- name: Compare screenshot test
run: ./gradlew compareRoborazziDebug --stacktrace
- name: Check for screenshot diffs
id: check-diffs
run: |
if find . -type f -name "*_compare.png" | grep -q .; then
echo "has_diffs=true" >> "$GITHUB_OUTPUT"
else
echo "has_diffs=false" >> "$GITHUB_OUTPUT"
fi
- name: Upload screenshot diff
if: ${{ !cancelled() && steps.check-diffs.outputs.has_diffs == 'true' }}
uses: actions/upload-artifact@v7
with:
name: screenshot-diff
path: '**/build/outputs/roborazzi'
retention-days: 30
- name: Upload diff reports
if: ${{ !cancelled() && steps.check-diffs.outputs.has_diffs == 'true' }}
uses: actions/upload-artifact@v7
with:
name: screenshot-diff-reports
path: '**/build/reports'
retention-days: 30
- name: Upload diff test results
if: ${{ !cancelled() && steps.check-diffs.outputs.has_diffs == 'true' }}
uses: actions/upload-artifact@v7
with:
name: screenshot-diff-test-results
path: '**/build/test-results'
retention-days: 30
- name: Push diffs to companion branch
id: push-screenshot-diff
if: steps.check-diffs.outputs.has_diffs == 'true'
env:
BRANCH_NAME: companion_${{ github.head_ref }}
run: |
git branch -D "$BRANCH_NAME" || true
git checkout --orphan "$BRANCH_NAME"
git rm -rf .
files_to_add=$(find . -type f -name "*_compare.png")
for file in $files_to_add; do
if [[ "$file" =~ ^[a-zA-Z0-9_./-]+$ ]]; then
git add "$file"
fi
done
git config --global user.name ScreenshotBot
git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com
git commit -m "Add screenshot diff"
git push origin HEAD:"$BRANCH_NAME" -f
- name: Generate diff report
id: generate-diff-reports
if: steps.check-diffs.outputs.has_diffs == 'true'
env:
BRANCH_NAME: companion_${{ github.head_ref }}
shell: bash
run: |
files=$(find . -type f -name "*_compare.png" | grep "roborazzi/" | grep -E "^[a-zA-Z0-9_./-]+$")
delimiter="$(openssl rand -hex 8)"
{
echo "reports<<${delimiter}"
echo "## Roborazzi Screenshot Diff Report"
echo "Comparing against base branch: ${{ github.event.pull_request.base.ref }}"
echo ""
echo "| File name | Image |"
echo "|-------|-------|"
} >> "$GITHUB_OUTPUT"
for file in $files; do
fileName=$(basename "$file" | sed -r 's/(.{20})/\1<br>/g')
echo "| [$fileName](https://github.com/${{ github.repository }}/blob/$BRANCH_NAME/$file) |  |" >> "$GITHUB_OUTPUT"
done
echo "${delimiter}" >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@v4
id: fc
if: steps.generate-diff-reports.outputs.reports != ''
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: Roborazzi Screenshot Diff Report
- name: Add or update comment on PR
uses: peter-evans/create-or-update-comment@v5
if: steps.generate-diff-reports.outputs.reports != ''
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: ${{ steps.generate-diff-reports.outputs.reports }}
edit-mode: replace
- name: Cleanup outdated companion branches
if: steps.check-diffs.outputs.has_diffs == 'true'
run: |
git fetch origin
git branch -r --format="%(refname:lstrip=3)" | grep companion_ | while read -r branch; do
last_commit_date_timestamp=$(git log -1 --format=%ct "origin/$branch")
now_timestamp=$(date +%s)
if [ $((now_timestamp - last_commit_date_timestamp)) -gt 2592000 ]; then
echo "Deleting outdated branch: $branch"
git push origin --delete "$branch"
fi
done
================================================
FILE: .github/workflows/daily-build.yml
================================================
name: Daily Build
on:
workflow_dispatch:
# Uncomment to enable scheduled daily builds
# schedule:
# - cron: '0 6 * * 1-5' # Weekdays at 6:00 AM UTC
permissions:
contents: write
env:
XCODE_VERSION: 16.4
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
TRAKT_CLIENT_SECRET: ${{ secrets.TRAKT_CLIENT_SECRET }}
TRAKT_REDIRECT_URI: ${{ secrets.TRAKT_REDIRECT_URI }}
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.bump.outputs.version }}
build: ${{ steps.bump.outputs.build }}
sha: ${{ steps.push.outputs.sha }}
steps:
- name: Checkout main
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN }}
- name: Setup Gradle Environment
uses: ./.github/actions/setup-gradle
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Bump build number
id: bump
run: |
./gradlew bumpVersion -Ptype=beta
version=$(grep 'VERSION_NUMBER' version.txt | sed 's/.*= *//')
build=$(grep 'BUILD_NUMBER' version.txt | sed 's/.*= *//')
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "build=$build" >> "$GITHUB_OUTPUT"
echo "Version: $version, BUILD_NUMBER: $build"
- name: Commit and push
id: push
run: |
git add version.txt
git commit -m "chore: bump daily build number to ${{ steps.bump.outputs.build }}"
git push origin main
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
build-android:
needs: prepare
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.sha }}
- name: Setup Android Release
uses: ./.github/actions/setup-android-release
with:
google-services-json: ${{ secrets.GOOGLE_SERVICES_JSON_RELEASE }}
signing-encrypt-key: ${{ secrets.SIGNING_ENCRYPT_KEY }}
- name: Build release AAB and APK
env:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
run: |
./gradlew :app:bundleRelease :app:assembleRelease \
-Pandroidx.baselineprofile.skipgeneration=true \
-Papp.debugOnly=false \
-Papp.versionSuffix=-dev \
-PreleaseStoreFile=release/app-release.jks \
-PreleaseStorePassword="$SIGNING_STORE_PASSWORD" \
-PreleaseKeyAlias=tvmaniac \
-PreleaseKeyPassword="$SIGNING_KEY_PASSWORD" \
--no-configuration-cache
- name: Print build info
run: |
echo "Version: $(grep 'VERSION_NUMBER' version.txt | sed 's/.*= *//')-dev"
echo "Build: $(grep 'BUILD_NUMBER' version.txt | sed 's/.*= *//')"
- name: Deploy to Play Store internal track
run: |
export PLAY_STORE_SERVICE_ACCOUNT_JSON=$(cat release/play-service-account.json)
bundle exec fastlane android deploy_play_store
- name: Distribute to Firebase App Distribution
env:
FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }}
run: |
if [ -n "$FIREBASE_APP_ID" ] && [ -f "release/firebase-sa.json" ]; then
export FIREBASE_APP_DISTRIBUTION_SA=release/firebase-sa.json
bundle exec fastlane android distribute_firebase
else
echo "warning: Firebase not configured — skipping distribution"
fi
build-ios:
needs: prepare
runs-on: macos-15
timeout-minutes: 60
env:
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 60
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.sha }}
- name: Setup iOS Release
uses: ./.github/actions/setup-ios-release
with:
xcode-version: ${{ env.XCODE_VERSION }}
google-service-info-plist: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_RELEASE }}
- name: Build and upload to TestFlight
env:
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
run: bundle exec fastlane ios build_beta
- name: Upload iOS artifacts
uses: actions/upload-artifact@v7
if: always()
with:
name: ios-daily
path: |
build/
fastlane/logs
================================================
FILE: .github/workflows/nightly-integration-tests.yml
================================================
name: Nightly Integration Tests
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
workflow_call:
concurrency:
group: nightly-integration-tests-${{ github.ref }}
cancel-in-progress: false
env:
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
TRAKT_CLIENT_SECRET: ${{ secrets.TRAKT_CLIENT_SECRET }}
TRAKT_REDIRECT_URI: ${{ secrets.TRAKT_REDIRECT_URI }}
jobs:
integration-tests:
name: "Pixel 6 API 34 Integration Tests"
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: 🚚 Checkout
uses: actions/checkout@v6
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: 🐘 Setup Gradle Environment
uses: ./.github/actions/setup-gradle
- name: Setup Android SDK
uses: android-actions/setup-android@v4
- name: Accept licenses
run: yes | sdkmanager --licenses || true
- name: 📦 Cache AVD
uses: actions/cache@v5
with:
path: |
~/.android/avd/*
~/.android/adb*
~/.android/debug.keystore
key: avd-pixel6Api34-${{ hashFiles('gradle/libs.versions.toml') }}
restore-keys: |
avd-pixel6Api34-
- name: 🔥 Install google-services.json
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: |
if [ -n "$GOOGLE_SERVICES_JSON" ]; then
echo "$GOOGLE_SERVICES_JSON" | base64 --decode > app/google-services.json
else
echo "warning: GOOGLE_SERVICES_JSON secret not set — Firebase will be disabled"
fi
- name: 🤖 Run Integration Tests on Emulator
run: |
./gradlew :app:pixel6Api34DebugAndroidTest \
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" \
--no-configuration-cache
- name: 📊 Upload Test Reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: integration-test-reports
path: |
app/build/reports/androidTests/
app/build/outputs/androidTest-results/
app/build/outputs/managed_device_android_test_additional_output/
if-no-files-found: ignore
================================================
FILE: .github/workflows/promote-release.yml
================================================
name: Promote Release
on:
# schedule:
# - cron: '0 5 * * *'
workflow_dispatch:
inputs:
android_rollout:
description: 'Android rollout percentage override (e.g., 0.5 for 50%)'
type: string
required: false
skip_ios:
description: 'Skip iOS App Store submission'
type: boolean
default: false
crash_free_threshold:
description: 'Minimum crash-free rate (%) to allow promotion'
type: string
default: '99.0'
permissions:
contents: read
env:
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
TRAKT_CLIENT_SECRET: ${{ secrets.TRAKT_CLIENT_SECRET }}
TRAKT_REDIRECT_URI: ${{ secrets.TRAKT_REDIRECT_URI }}
jobs:
check-rollout:
runs-on: ubuntu-latest
outputs:
should_promote: ${{ steps.check.outputs.should_promote }}
next_rollout: ${{ steps.check.outputs.next_rollout }}
current_rollout: ${{ steps.check.outputs.current_rollout }}
release_tag: ${{ steps.check.outputs.release_tag }}
days_since_release: ${{ steps.check.outputs.days_since_release }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Determine rollout state
id: check
run: |
if [ -n "${{ inputs.android_rollout }}" ]; then
echo "should_promote=true" >> "$GITHUB_OUTPUT"
echo "next_rollout=${{ inputs.android_rollout }}" >> "$GITHUB_OUTPUT"
echo "current_rollout=manual" >> "$GITHUB_OUTPUT"
echo "days_since_release=manual" >> "$GITHUB_OUTPUT"
latest_tag=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -1)
echo "release_tag=$latest_tag" >> "$GITHUB_OUTPUT"
exit 0
fi
latest_tag=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -1)
if [ -z "$latest_tag" ]; then
echo "No release tags found"
echo "should_promote=false" >> "$GITHUB_OUTPUT"
exit 0
fi
tag_epoch=$(git log -1 --format=%ct "$latest_tag")
now_epoch=$(date +%s)
days_since=$(( (now_epoch - tag_epoch) / 86400 ))
# Rollout schedule: Day 0=0.1%, Day 1=1%, Day 3=10%, Day 5=50%, Day 7=100%
if [ "$days_since" -ge 7 ]; then
next_rollout="1.0"
elif [ "$days_since" -ge 5 ]; then
next_rollout="0.5"
elif [ "$days_since" -ge 3 ]; then
next_rollout="0.1"
elif [ "$days_since" -ge 1 ]; then
next_rollout="0.01"
else
echo "Day 0: release just deployed at 0.1%, no promotion needed yet"
echo "should_promote=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "should_promote=true" >> "$GITHUB_OUTPUT"
echo "next_rollout=$next_rollout" >> "$GITHUB_OUTPUT"
echo "days_since_release=$days_since" >> "$GITHUB_OUTPUT"
echo "release_tag=$latest_tag" >> "$GITHUB_OUTPUT"
echo "Release: $latest_tag, Days since: $days_since, Next rollout: $next_rollout"
check-vitals:
needs: check-rollout
if: ${{ needs.check-rollout.outputs.should_promote == 'true' }}
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.vitals.outputs.passed }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Decrypt Play Store service account
env:
SIGNING_ENCRYPT_KEY: ${{ secrets.SIGNING_ENCRYPT_KEY }}
run: |
openssl aes-256-cbc -d -pbkdf2 \
-in release/play-service-account.aes \
-out release/play-service-account.json \
-k "$SIGNING_ENCRYPT_KEY"
- name: Check Play Vitals crash-free Rate
id: vitals
run: |
THRESHOLD="${{ inputs.crash_free_threshold }}"
THRESHOLD="${THRESHOLD:-99.0}"
APP_PACKAGE="com.thomaskioko.tvmaniac"
if ! gcloud auth activate-service-account --key-file=release/play-service-account.json 2>/dev/null; then
echo "::warning::Could not authenticate — skipping vitals check"
echo "passed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
ACCESS_TOKEN=$(gcloud auth print-access-token 2>/dev/null || true)
if [ -z "$ACCESS_TOKEN" ]; then
echo "::warning::Could not obtain access token — skipping vitals check"
echo "passed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
END_DATE=$(date -u +%Y-%m-%d)
START_DATE=$(date -u -d "7 days ago" +%Y-%m-%d)
RESPONSE=$(curl -sf \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-X POST \
"https://playdeveloperreporting.googleapis.com/v1beta1/apps/${APP_PACKAGE}/crashRateMetricSet:query" \
-d "{
\"timelineSpec\": {
\"aggregationPeriod\": \"DAILY\",
\"startTime\": {
\"year\": $(date -u -d "$START_DATE" +%Y),
\"month\": $(date -u -d "$START_DATE" +%-m),
\"day\": $(date -u -d "$START_DATE" +%-d)
},
\"endTime\": {
\"year\": $(date -u -d "$END_DATE" +%Y),
\"month\": $(date -u -d "$END_DATE" +%-m),
\"day\": $(date -u -d "$END_DATE" +%-d)
}
},
\"metrics\": [\"crashRate\"],
\"dimensions\": []
}" 2>/dev/null) || true
if [ -z "$RESPONSE" ]; then
echo "::warning::Play Vitals API unavailable — skipping crash check"
echo "::warning::To enable: activate the Play Developer Reporting API and grant the service account 'View app information' permission"
echo "passed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
CRASH_RATE=$(echo "$RESPONSE" | jq -r '[.rows[].metrics[0].decimalValue.value // "0"] | last' 2>/dev/null || echo "")
if [ -z "$CRASH_RATE" ] || [ "$CRASH_RATE" = "null" ]; then
echo "::warning::Could not parse crash rate — skipping check"
echo "passed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
CRASH_FREE=$(echo "scale=2; (1 - $CRASH_RATE) * 100" | bc -l)
echo "Crash-free rate: ${CRASH_FREE}% (threshold: ${THRESHOLD}%)"
echo "crash_free_rate=${CRASH_FREE}" >> "$GITHUB_OUTPUT"
if (( $(echo "$CRASH_FREE < $THRESHOLD" | bc -l) )); then
echo "::error::Crash-free rate ${CRASH_FREE}% is below threshold ${THRESHOLD}% — blocking promotion"
echo "passed=false" >> "$GITHUB_OUTPUT"
else
echo "passed=true" >> "$GITHUB_OUTPUT"
fi
promote-android:
needs: [check-rollout, check-vitals]
if: ${{ needs.check-rollout.outputs.should_promote == 'true' && needs.check-vitals.outputs.passed == 'true' }}
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Decrypt Play Store service account
env:
SIGNING_ENCRYPT_KEY: ${{ secrets.SIGNING_ENCRYPT_KEY }}
run: |
openssl aes-256-cbc -d -pbkdf2 -in release/play-service-account.aes -out release/play-service-account.json -k "$SIGNING_ENCRYPT_KEY"
- name: Promote Android release
run: |
export PLAY_STORE_SERVICE_ACCOUNT_JSON=$(cat release/play-service-account.json)
bundle exec fastlane android promote \
from:production \
to:production \
rollout:${{ needs.check-rollout.outputs.next_rollout }}
- name: Summary
run: |
echo "### Android Promotion" >> $GITHUB_STEP_SUMMARY
echo "- **Release**: ${{ needs.check-rollout.outputs.release_tag }}" >> $GITHUB_STEP_SUMMARY
echo "- **Days since release**: ${{ needs.check-rollout.outputs.days_since_release }}" >> $GITHUB_STEP_SUMMARY
echo "- **Rollout**: ${{ needs.check-rollout.outputs.next_rollout }}" >> $GITHUB_STEP_SUMMARY
echo "- **Vitals**: crash-free ${{ needs.check-vitals.outputs.crash_free_rate || 'N/A' }}%" >> $GITHUB_STEP_SUMMARY
promote-ios:
needs: [check-rollout, check-vitals]
if: ${{ needs.check-rollout.outputs.should_promote == 'true' && !inputs.skip_ios }}
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Submit to App Store
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
IOS_SUBMIT_FOR_REVIEW: "true"
run: bundle exec fastlane ios deploy_app_store
- name: Summary
run: |
echo "### iOS Promotion" >> $GITHUB_STEP_SUMMARY
echo "- **Release**: ${{ needs.check-rollout.outputs.release_tag }}" >> $GITHUB_STEP_SUMMARY
echo "- **Action**: Submitted for App Store review" >> $GITHUB_STEP_SUMMARY
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
permissions:
contents: write
env:
XCODE_VERSION: 16.4
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
TRAKT_CLIENT_SECRET: ${{ secrets.TRAKT_CLIENT_SECRET }}
TRAKT_REDIRECT_URI: ${{ secrets.TRAKT_REDIRECT_URI }}
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.info.outputs.version }}
changelog: ${{ steps.changelog.outputs.body }}
steps:
- name: Checkout tag
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Gradle Environment
uses: ./.github/actions/setup-gradle
- name: Read version info
id: info
run: |
version=$(grep 'VERSION_NUMBER' version.txt | sed 's/.*= *//')
build=$(grep 'BUILD_NUMBER' version.txt | sed 's/.*= *//')
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "build=$build" >> "$GITHUB_OUTPUT"
echo "Version: $version, Build: $build, Tag: ${{ github.ref_name }}"
- name: Validate tag matches version.txt
run: |
tag="${{ github.ref_name }}"
version=$(grep 'VERSION_NUMBER' version.txt | sed 's/.*= *//')
expected_tag="v${version}"
if [ "$tag" != "$expected_tag" ]; then
echo "::error::Tag '$tag' does not match version.txt version '$expected_tag'"
exit 1
fi
- name: Generate release notes
id: changelog
run: |
body=$(git cliff --current --strip header 2>/dev/null || echo "Release ${{ github.ref_name }}")
{
echo 'body<<CHANGELOG_EOF'
echo "$body"
echo 'CHANGELOG_EOF'
} >> "$GITHUB_OUTPUT"
build-android:
needs: prepare
runs-on: ubuntu-latest
steps:
- name: Checkout tag
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Android Release
uses: ./.github/actions/setup-android-release
with:
google-services-json: ${{ secrets.GOOGLE_SERVICES_JSON_RELEASE }}
signing-encrypt-key: ${{ secrets.SIGNING_ENCRYPT_KEY }}
- name: Build release AAB and APK
env:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
run: |
./gradlew :app:bundleRelease :app:assembleRelease \
-Pandroidx.baselineprofile.skipgeneration=true \
-Papp.debugOnly=false \
-Papp.versionSuffix= \
-PreleaseStoreFile=release/app-release.jks \
-PreleaseStorePassword="$SIGNING_STORE_PASSWORD" \
-PreleaseKeyAlias=tvmaniac \
-PreleaseKeyPassword="$SIGNING_KEY_PASSWORD" \
--no-configuration-cache
- name: Write release notes
env:
RELEASE_CHANGELOG: ${{ needs.prepare.outputs.changelog }}
run: |
mkdir -p fastlane/metadata/android/en-US/changelogs
echo "$RELEASE_CHANGELOG" > fastlane/metadata/android/en-US/changelogs/default.txt
- name: Deploy to Play Store production (0.1% rollout)
run: |
export PLAY_STORE_SERVICE_ACCOUNT_JSON=$(cat release/play-service-account.json)
bundle exec fastlane android deploy_production rollout:0.001
- name: Distribute to Firebase App Distribution
env:
FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }}
run: |
if [ -n "$FIREBASE_APP_ID" ] && [ -f "release/firebase-sa.json" ]; then
export FIREBASE_APP_DISTRIBUTION_SA=release/firebase-sa.json
bundle exec fastlane android distribute_firebase
else
echo "warning: Firebase not configured - skipping distribution"
fi
- name: Upload Android artifacts
uses: actions/upload-artifact@v7
with:
name: android-release
path: |
app/build/outputs/bundle/release/*.aab
app/build/outputs/apk/release/*.apk
build-ios:
needs: prepare
runs-on: macos-15
timeout-minutes: 60
env:
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 60
steps:
- name: Checkout tag
uses: actions/checkout@v6
- name: Setup iOS Release
uses: ./.github/actions/setup-ios-release
with:
xcode-version: ${{ env.XCODE_VERSION }}
google-service-info-plist: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_RELEASE }}
- name: Build and upload to TestFlight
env:
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
run: bundle exec fastlane ios build_release
- name: Upload iOS artifacts
uses: actions/upload-artifact@v7
if: always()
with:
name: ios-release
path: |
build/
fastlane/logs
create-github-release:
needs: [prepare, build-android, build-ios]
if: ${{ always() && (needs.build-android.result == 'success' || needs.build-ios.result == 'success') }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download Android artifacts
if: ${{ needs.build-android.result == 'success' }}
uses: actions/download-artifact@v8
with:
name: android-release
path: artifacts/android
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${{ github.ref_name }}"
version="${tag#v}"
release_args=(
"$tag"
--title "Release $version"
--notes "${{ needs.prepare.outputs.changelog }}"
--draft
)
if [ -d "artifacts/android" ]; then
apk=$(find artifacts/android -name "*.apk" -type f | head -1)
if [ -n "$apk" ]; then
release_args+=("$apk")
fi
fi
gh release create "${release_args[@]}"
================================================
FILE: .github/workflows/store-screenshot.yml
================================================
name: Store Screenshot
on:
push:
branches:
- main
permissions: {}
env:
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
TRAKT_CLIENT_SECRET: ${{ secrets.TRAKT_CLIENT_SECRET }}
TRAKT_REDIRECT_URI: ${{ secrets.TRAKT_REDIRECT_URI }}
jobs:
store-screenshot-test:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read # for clone
actions: write # for upload-artifact
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Gradle Environment
uses: ./.github/actions/setup-gradle
- name: Record screenshot
id: record-test
run: |
# Use --rerun-tasks to disable cache for test task
./gradlew recordRoborazziDebug --stacktrace --rerun-tasks
- uses: actions/upload-artifact@v7
if: ${{ always() }}
with:
name: screenshot
path: |
**/build/outputs/roborazzi
retention-days: 30
- uses: actions/upload-artifact@v7
if: ${{ always() }}
with:
name: screenshot-reports
path: |
**/build/reports
retention-days: 30
- uses: actions/upload-artifact@v7
if: ${{ always() }}
with:
name: screenshot-test-results
path: |
**/build/test-results
retention-days: 30
================================================
FILE: .gitignore
================================================
# Built application files
*.apk
*.ap_
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Gradle files
.gradle/
build/
.kotlin/
/build
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# Android Studio
*.iws
*.ipr
*.iml
.gradle
/local.properties
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
/.idea/*
!/.idea/codeStyles
!/.idea/dictionaries
# Eclipse project files
.classpath
.project
## Playgrounds
timeline.xctimeline
playground.xcworkspace
## Shared
/shared/domain/settings/implementation/*.preferences_pb
*.xcworkspacedata
*.xcuserstate
xcschememanagement.plist
*.xcbkptlist
## User settings
xcuserdata/
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
*.xcuserstate
###### FastLane #######
fastlane/report.xml
fastlane/Preview.html
fastlane/test_output
fastlane/builds
/ios/fastlane/report.xml
ios/build
ios/SourcePackages/
derived_data/
*.xcresult
/.junie/guidelines.md
/tasks
CLAUDE.md
.claude/
# Configuration files with secrets
**/dev.yaml
**/prod.yaml
!**/*.yaml.template
**/.build/
**/Package.resolved
# Firebase
google-services.json
GoogleService-Info.plist
# Release signing
release/app-release.jks
release/play-service-account.json
release/firebase-sa.json
release/signing.properties
================================================
FILE: .idea/codeStyles/Project.xml
================================================
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="tv-maniac" />
</state>
</component>
================================================
FILE: .idea/dictionaries/project.xml
================================================
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>tmdb</w>
<w>trakt</w>
</words>
</dictionary>
</component>
================================================
FILE: .ruby-version
================================================
3.3.0
================================================
FILE: .swiftformat
================================================
--indent 4
--exclude ios/build,ios/Modules/*/.build,**/.build,**/build,**/ios-framework/build,ios/SourcePackages,ios/derived_data,**/SourcePackages,**/derived_data,**/checkouts
================================================
FILE: .swiftlint.yml
================================================
cyclomatic_complexity:
warning: 15
error: 20
function_body_length:
warning: 75
error: 100
disabled_rules:
- todo
- trailing_whitespace
- trailing_comma
- trailing_newline
- identifier_name
- type_name
- multiple_closures_with_trailing_closure
- type_body_length
- line_length
- opening_brace
- leading_whitespace
- void_function_in_ternary
opt_in_rules:
- sorted_imports
# Files & Folder not to check
excluded:
- .build
- "**/build"
- "**/.build"
- "**/checkouts"
- "**/SourcePackages"
- derived_data
- DerivedData
- ios/Modules/SnapshotTestingLib
- ios/build
- vendor/bundle
- ios-framework/build
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
## [0.1.2] - 2026-03-25
### Bug Fixes
- **Android**: Fix SnackBar shown in error state.
- **auth**: Update Trakt token endpoint and client authentication method
- update Firebase distribution path to use release APK
- fix linting
### CI/CD
- Ignore iOS tests in watchlist module
### Features
- **i18n**: Add new strings for marking episodes
- **ios**: Configure background processing and fetch modes
- enable experimental R8 optimized resource shrinking
- enable mapping file upload for Firebase Crashlytics in release build
- Add query to get the latest season for followed shows
- Add episode notification scheduling tasks
- Add 'Auto' image quality setting and optimize image loading
- Add `season_numbers` to `TvShow` SQL queries
- Refactor theme implementation in settings
### Miscellaneous
- update release workflow to use release-specific secrets and add signing files to .gitignore
- add encrypted signing keys
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to TvManiac
TvManiac is a personal learning playground for Kotlin Multiplatform development. Contributions that fix bugs,
improve documentation, or demonstrate KMP patterns are welcome.
## Prerequisites
| Tool | Version |
|---|---|
| JDK | 21 (Zulu recommended: https://www.azul.com/downloads/?package=jdk#zulu) |
| Android Studio | Latest stable or canary |
| Xcode | 16.4 (iOS contributions) |
| KMM Plugin | Latest compatible with the Kotlin version in `gradle/libs.versions.toml` |
CocoaPods is not used. The iOS app consumes the shared KMP framework as an XCFramework built by Gradle.
## Cloning and Initial Setup
```bash
git clone https://github.com/thomaskioko/tv-maniac.git
cd tv-maniac
./scripts/install-git-hooks.sh
```
The git hooks run Spotless formatting checks before each commit. A commit is blocked if formatting fails.
Fix formatting with `./gradlew spotlessApply` and then re-commit.
### API Keys
The app requires TMDB and Trakt credentials at build time. See [docs/setup.md](docs/setup.md) for how to
obtain them. Create `local.properties` in the project root:
```properties
TMDB_API_KEY=your_tmdb_api_key
TRAKT_CLIENT_ID=your_trakt_client_id
TRAKT_CLIENT_SECRET=your_trakt_client_secret
TRAKT_REDIRECT_URI=tvmaniac://callback
```
This file is gitignored. Do not commit it.
## Building
**Android (debug):**
```bash
./gradlew :app:assembleDebug
```
For a faster local build that skips the iOS XCFramework:
```bash
./gradlew assembleDebug -Ptvmaniac.debugOnly=true
```
**iOS:**
Open `ios/tv-maniac.xcodeproj` in Xcode 16.4, select a simulator or device, and run.
## Running Tests
```bash
# JVM unit tests (fast, no device needed)
./gradlew jvmTest
# Android connected tests (requires a running emulator or device)
./gradlew connectedAndroidTest
# iOS simulator tests
./gradlew iosSimulatorArm64Test -Papp.enableIos=true
```
## Code Style
**Kotlin:**
- 2-space indentation, 140-character line length.
- Use `ImmutableList` and `toImmutableList()` from `kotlinx.collections.immutable` for state classes.
- No try-catch blocks that silently swallow errors. Propagate exceptions up to the presentation layer.
- Fakes, not mocks. Each `data/*/testing` module provides fake implementations for tests.
- Test names follow the pattern: `should X given Y`. Do not include function names in test names.
- Spotless enforces formatting. Run `./gradlew spotlessApply` before pushing.
**Swift:**
- Follow the Swift API Design Guidelines.
- Format with SwiftFormat. Check with `fastlane ios check_swift_format` before pushing.
- Do not add business logic to SwiftUI views. Views consume shared KMP presenters only.
**General:**
- No comments in code unless the intent cannot be expressed through naming.
- Always specify access modifiers on all Kotlin declarations.
- Business logic belongs in shared KMP code (`domain/*`, `data/*`), never in platform UI layers.
## Branching and Pull Requests
1. Fork the repository and create your branch from `main`.
2. Keep branches focused on a single concern. Large refactors should be discussed in an issue first.
3. Ensure CI passes: Spotless, unit tests, and lint checks must all be green.
4. Include a clear description in your PR: what changed, why, and how to test it.
5. Reference any related issue with `Closes #123` or `Relates to #123` in the PR body.
PRs that add new architectural patterns should also update the relevant file under `docs/architecture/`.
## Architecture Overview
Before contributing, read the architecture docs so your change fits the existing patterns:
- [Modularization](docs/architecture/modularization.md)
- [Presentation Layer](docs/architecture/presentation-layer.md)
- [Data Layer](docs/architecture/data-layer.md)
- [Navigation](docs/architecture/navigation.md)
- [Dependency Injection](docs/architecture/dependency-injection.md)
## Filing Bugs and Questions
Open an issue on GitHub. Use the bug report template for reproducible defects and the feature request
template for new ideas. For open-ended questions, open a GitHub issue with the `question` label.
================================================
FILE: GEMINI.md
================================================
# TV Maniac Agent Rules
## Project Overview
TV Maniac is a Kotlin Multiplatform (KMP) project for tracking TV shows. It follows a highly modularized Clean Architecture with a strict separation of concerns.
## Architecture & Tech Stack
- **KMP**: Shared business logic, state management, and data layer.
- **Clean Architecture**: Organized into `data`, `domain`, `presenter`, and `ui` layers.
- **Metro**: A custom compile-time Dependency Injection (DI) system.
- **Decompose**: Used for shared navigation and lifecycle management.
- **Store 5**: Used for data fetching (Fetcher) and caching (SourceOfTruth).
- **SQLDelight**: Local persistence.
- **UI**: Jetpack Compose for Android, SwiftUI for iOS.
## Core Mandates & Conventions
### 1. Modularization & Dependencies
- **Strict API/Implementation Split**: Most modules are split into `:api` and `:implementation` (or implicit).
- **Rule**: Modules MUST depend on `api` modules of other features, never `implementation` (except for entry points like `:app` and `:ios-framework`).
- **Feature Structure**: Features follow a 3-module split:
- `nav`: Contains routes, parameters, and navigation-related DI.
- `presenter`: Contains business logic, state management (MVI), and domain interactors.
- `ui`: Contains platform-specific UI (Compose).
### 2. Dependency Injection (Metro)
- Use `@DependencyGraph`, `@GraphExtension`, and `@BindingContainer` for DI.
- **Naming**:
- DI interfaces MUST use the `*Graph` suffix.
- Binding providers MUST use the `*BindingContainer` suffix.
- **Scopes**:
- `AppScope`: Singleton/Application lifetime.
- `ActivityScope`: Activity lifetime.
- `ScreenScope`: Decompose component lifetime.
### 3. Navigation (Decompose)
- Use `NavRoute` for standard navigation and `SheetConfig` for bottom sheets.
- **Annotations**:
- `@NavScreen`: Annotate presenters for standard screens.
- `@NavSheet`: Annotate presenters for bottom sheets.
- `@TabScreen`: Annotate presenters for tab screens.
- **Codegen**: Navigation is largely handled via codegen based on these annotations.
### 4. Presentation Layer (MVI)
- **Presenters**:
- MUST use `@AssistedInject` and have an `@AssistedFactory`.
- MUST extend `ComponentContext` (via delegation).
- MUST expose state as `Value<State>` (for Decompose) or `StateFlow<State>`.
- MUST use a `dispatch(Action)` function for UI events.
- **Naming**: `*Presenter`, `*Action`, `*Content` (for state).
- **Standardized Helpers**:
- Use `ObservableLoadingCounter` for tracking loading states.
- Use `UiMessageManager` for managing transient UI messages (errors, snackbars).
- Use `.collectStatus()` extension to handle flow statuses and pipe errors/loading to the above managers.
### 5. Data Layer (Store)
- Use `Store 5` for fetching and caching.
- `Fetcher`: For network requests (Ktor).
- `SourceOfTruth`: For local caching (SQLDelight).
- `Validator`: To determine if cached data is still valid.
### 6. Localization
- **Rule**: Never use hardcoded strings or platform-specific string resources in shared code.
- Use `Localizer` interface for shared string resolution.
- Use `MR` (MOKO Resources) for string and plural definitions.
- In Compose UI, use `resource.resolve(LocalContext.current)` or similar helpers.
### 7. Coding Style
- **Naming**: Use descriptive names. Suffix classes with their role (e.g., `Interactor`, `Repository`, `Presenter`).
- **Types**: Prefer explicit types for public APIs.
- **Immutability**: Use `kotlinx.collections.immutable` for state collections.
### 8. Testing
- **Prefer Fakes over Mocks**: Use hand-written fakes for repositories and interactors.
- **Testing Modules**: Fakes should reside in a `:testing` module corresponding to the feature or layer.
- **Navigator**: Use `TestNavigator` for asserting navigation events in presenter tests.
- **Turbine**: Use Turbine for testing flows.
### 9. Integration Testing
- **Location**: Integration tests reside in the `:core:integration` module.
- **TestGraph**: Use `TestGraph` to resolve dependencies. It provides a real dependency graph with specific components replaced by fakes (e.g., Network, DataStore).
- **Execution**:
- Use `runTestWithGraph { graph -> ... }` to run tests. This helper handles setting up the `TestGraph` and the `TestDispatcher`.
- Ensure `Lifecycle.destroy()` is called and `advanceUntilIdle()` is used when testing presenters to prevent coroutine leaks.
- **Platform Specifics**: Metro's graph factories are materialized per target. JVM and iOS variants of helpers must live in their respective source sets.
- **Android**: Use `androidHostTest` for running integration tests on the JVM while having access to Android resources if needed.
## Development Workflow
1. **Feature Addition**:
- Define `Route` in `nav`.
- Implement `Interactor` in `domain` (if needed).
- Implement `Presenter` in `presenter` using `@AssistedInject` and `@NavScreen`.
- Implement `Screen` in `ui` using `@ScreenUi`.
2. **Data Changes**:
- Update SQLDelight `.sq` files.
- Update `Store` configuration.
- Update `Mapper` to convert database/network entities to domain/UI models.
## Verification
- Run `./gradlew lint` for Android linting.
- Run `./gradlew test` for unit tests.
- Ensure all DI graphs are valid by running a build.
================================================
FILE: Gemfile
================================================
source "https://rubygems.org"
gem "fastlane"
gem "xcode-install"
gem "xcpretty"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
<p align="center">
<img src="art/TvManiacBanner.png" width="100%" />
</p>
# TvManiac




[](https://github.com/thomaskioko/tv-maniac/releases/latest)
**TvManiac** is a personalized entertainment tracking and recommendation Multiplatform app (Android & iOS) for tracking TV Shows. By utilizing [Trakt](https://trakt.tv) and [TMDB](https://developer.themoviedb.org/docs), you can discover shows, manage your watchlist, track watch progress, and get personalized recommendations.
| Android | iOS |
|---|---|
| <video src="https://github.com/user-attachments/assets/90ec7924-7d50-40a4-bb0b-89d79aa9bbcd" width=350/> | <video src="https://github.com/user-attachments/assets/69f101b7-e100-4775-9893-6687e455560c" width=350/> |
> **Under Heavy Development**
>
> This is my playground for learning Kotlin Multiplatform. With that said, I'm sure it's filled with bugs crawling everywhere, and I'm probably doing a couple of things wrong. So a lot is changing, but that shouldn't stop you from checking it out.
## Install
Download the latest APK from [GitHub Releases](https://github.com/thomaskioko/tv-maniac/releases).
Join the open beta on [Google Play](https://play.google.com/store/apps/details?id=com.thomaskioko.tvmaniac&hl=en_US) or stay up to date with daily builds via Firebase:
[<img width=400 src="art/FirebaseAppDistribution.svg"/>](https://appdistribution.firebase.dev/i/564c934cc970634b)
---
## Getting Started
### Requirements
- [Zulu Java 21](https://www.azul.com/downloads/?package=jdk#zulu)
- Latest [Android Studio](https://developer.android.com/studio/preview)
- [KMM Plugin](https://kotlinlang.org/docs/multiplatform-mobile-setup.html)
### API Keys
The app requires TMDB and Trakt API credentials. See [docs/setup.md](docs/setup.md) for detailed instructions.
Create `local.properties` in the project root:
```properties
TMDB_API_KEY=your_tmdb_api_key
TRAKT_CLIENT_ID=your_trakt_client_id
TRAKT_CLIENT_SECRET=your_trakt_client_secret
TRAKT_REDIRECT_URI=tvmaniac://callback
```
### Setup & Build
```bash
./scripts/install-git-hooks.sh
```
**Android:**
```bash
./gradlew :app:assembleDebug
```
**iOS:**
Open `ios/tv-maniac.xcodeproj` in Xcode and run.
---
## Architecture
The project follows Clean Architecture with a modular design organized by feature and layer. Business logic and state management live in shared KMP code, while Android (Compose) and iOS (SwiftUI) contain only UI rendering.
For detailed documentation:
- [Modularization](docs/architecture/modularization.md)
- [Presentation Layer](docs/architecture/presentation-layer.md)
- [Data Layer](docs/architecture/data-layer.md)
- [Navigation](docs/architecture/navigation.md)
- [Dependency Injection](docs/architecture/dependency-injection.md)
- [Integration Testing](docs/architecture/integration-testing.md)
---
## Key Concepts
A few foundational libraries and patterns drive the architecture.
- **[Decompose](https://arkivanov.github.io/Decompose/)**. Shared navigation and lifecycle for KMP. The navigation stack, child components, and back handling all live in shared Kotlin code. Android (Compose) and iOS (SwiftUI) only render the active child. See [Navigation](docs/architecture/navigation.md).
- **[Metro](https://zacsweers.github.io/metro/latest/)**. Compile time dependency injection. There is no KSP processor and no runtime reflection. Modules expose interfaces from `api/` packages, implementations are bound with `@ContributesBinding`, and the full graph is assembled at the app entry point. See [Dependency Injection](docs/architecture/dependency-injection.md).
- **[Store pattern](https://store.mobilenativefoundation.org/)**. One fetch and cache pipeline per data type. A `Store` combines a `Fetcher` (network), a `SourceOfTruth` (SQLDelight DAO), and a `Validator` (cache freshness via `RequestManager`). Presenters never call the network or DAO directly. See [Data Layer](docs/architecture/data-layer.md).
- **Interactor and SubjectInteractor**. Thin orchestration in the domain layer. An `Interactor` runs a one shot action (mark watched, sign in). A `SubjectInteractor` exposes a continuous `Flow` of data (observe show details). Presenters compose these into screen state. See [Presentation Layer](docs/architecture/presentation-layer.md).
---
## Tech Stack
Architectural choices (Decompose, Metro, Store) are described in [Key Concepts](#key-concepts) above. The libraries below cover the rest of the shared and platform stack.
**Shared (KMP)**
- [Kotlin Coroutines](https://github.com/Kotlin/kotlinx.coroutines) - Concurrency
- [Ktor](https://ktor.io/) - Networking
- [SQLDelight](https://github.com/cashapp/sqldelight) - Local database
- [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) - JSON serialization
- [Multiplatform Paging](https://github.com/cashapp/multiplatform-paging) - Pagination
**Android**
- [Jetpack Compose](https://developer.android.com/jetpack/compose) - UI toolkit
- [Coil](https://coil-kt.github.io/coil/) - Image loading
- [AppAuth](https://openid.github.io/AppAuth-Android/) - OAuth authentication
**iOS**
- [SwiftUI](https://developer.apple.com/xcode/swiftui/) - UI framework
- [Nuke](https://github.com/kean/Nuke) - Image loading
- [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) - OAuth authentication
---
## Gradle Convention Plugins
Build configurations are managed by [app-gradle-plugins](https://github.com/thomaskioko/app-gradle-plugins), a set of custom Gradle convention plugins published to Maven Central. They handle Android/KMP module setup, versioning, release automation, and R8 optimization. For a deep dive into how they work, see [Publishing Gradle Convention Plugins](https://thomaskioko.me/posts/publishing_gradle_plugins/).
---
## References & Inspiration
- [Design Inspiration](https://dribbble.com/shots/7591814-HBO-Max-Companion-App-Animation)
- [Tivi](https://github.com/chrisbanes/tivi)
- [Compose Samples](https://github.com/android/compose-samples)
## License
```
Copyright 2021 Thomas Kioko
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
================================================
FILE: android-designsystem/build.gradle.kts
================================================
plugins { alias(libs.plugins.app.android) }
scaffold {
android {
enableAndroidResources()
useCompose()
useRoborazzi()
}
optIn("androidx.compose.material3.ExperimentalMaterial3Api")
}
dependencies {
api(libs.androidx.compose.ui.tooling)
api(libs.androidx.compose.ui.tooling.preview)
api(libs.androidx.compose.material3)
api(libs.androidx.compose.ui.ui)
api(libs.androidx.compose.material.icons)
api(libs.androidx.compose.runtime)
api(projects.domain.theme)
implementation(projects.core.testTags)
implementation(projects.i18n.generator)
api(libs.coil.base)
implementation(libs.androidx.annotation)
implementation(libs.androidx.collections)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.constraintlayout)
implementation(libs.coil.coil)
implementation(libs.coil.compose)
implementation(libs.kenburns)
implementation(libs.androidx.palette)
implementation(libs.coroutines.jvm)
implementation(libs.kotlinx.collections)
testImplementation(libs.robolectric.annotations)
testImplementation(projects.core.screenshotTests)
}
================================================
FILE: android-designsystem/src/debug/res/values/strings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">TvManiac</string>
</resources>
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Background.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.LocalAbsoluteTonalElevation
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.compose.theme.LocalBackgroundTheme
/**
* The main background for the app. Uses [LocalBackgroundTheme] to set the color and tonal elevation
* of a [Surface].
*
* @param modifier Modifier to be applied to the background.
* @param content The background content.
*/
@Composable
public fun TvManiacBackground(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val color = LocalBackgroundTheme.current.color
val tonalElevation = LocalBackgroundTheme.current.tonalElevation
Surface(
color = if (color == Color.Unspecified) Color.Transparent else color,
tonalElevation = if (tonalElevation == Dp.Unspecified) 0.dp else tonalElevation,
modifier = modifier.fillMaxSize(),
) {
CompositionLocalProvider(LocalAbsoluteTonalElevation provides 0.dp) { content() }
}
}
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light Theme", showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Theme", showBackground = true)
public annotation class ThemePreviews
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun BackgroundDefault() {
Spacer(Modifier.size(100.dp))
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/BadgeChip.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.compose.components.ThemePreviews
import com.thomaskioko.tvmaniac.compose.components.TvManiacPreviewWrapperProvider
@Composable
public fun PremiereBadge(
modifier: Modifier = Modifier,
text: String,
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.onSurface,
) {
Text(
text = text,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.background,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
)
}
}
@Composable
public fun NewBadge(
modifier: Modifier = Modifier,
text: String,
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.secondary,
) {
Text(
text = text,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondary,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
)
}
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun PremiereBadgePreview() {
PremiereBadge(text = "Premiere")
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun NewBadgePreview() {
NewBadge(text = "New")
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Buttons.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LibraryAddCheck
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.compose.extensions.iconButtonBackgroundScrim
import com.thomaskioko.tvmaniac.compose.theme.TvManiacTheme
import com.thomaskioko.tvmaniac.domain.theme.Theme
@Composable
public fun FilledTextButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = RectangleShape,
buttonColors: ButtonColors = ButtonDefaults.textButtonColors(),
content: @Composable RowScope.() -> Unit,
) {
TextButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = buttonColors,
content = content,
shape = shape,
)
}
@Composable
public fun FilledVerticalIconButton(
text: String,
onClick: () -> Unit,
imageVector: ImageVector,
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = RectangleShape,
style: TextStyle = MaterialTheme.typography.bodyMedium,
containerColor: Color = MaterialTheme.colorScheme.secondary,
contentColor: Color = MaterialTheme.colorScheme.onSecondary,
) {
TextButtonContent(
onClick = onClick,
modifier = modifier,
enabled = enabled,
containerColor = containerColor,
shape = shape,
content = {
Column(
modifier = Modifier
.sizeIn(minWidth = 120.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = imageVector,
contentDescription = null,
tint = when {
enabled -> contentColor
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
},
)
Text(
modifier = Modifier.padding(top = 2.dp),
text = text,
style = style,
color = when {
enabled -> contentColor
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
},
)
}
},
)
}
@Composable
public fun FilledHorizontalIconButton(
text: String,
onClick: () -> Unit,
imageVector: ImageVector,
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = RectangleShape,
style: TextStyle = MaterialTheme.typography.bodyMedium,
containerColor: Color = MaterialTheme.colorScheme.secondary,
) {
TextButtonContent(
onClick = onClick,
modifier = modifier,
enabled = enabled,
containerColor = containerColor,
shape = shape,
content = {
Row(
modifier = Modifier
.sizeIn(minHeight = 32.dp, minWidth = 140.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = imageVector,
contentDescription = null,
tint = when {
enabled -> MaterialTheme.colorScheme.onSecondary
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
},
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = text,
style = style,
color = when {
enabled -> MaterialTheme.colorScheme.onSecondary
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
},
)
}
},
)
}
@Composable
private fun TextButtonContent(
onClick: () -> Unit,
enabled: Boolean,
containerColor: Color,
shape: Shape,
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
TextButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onBackground,
containerColor = containerColor,
),
shape = shape,
) {
content()
}
}
@Composable
public fun HorizontalOutlinedButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
textPadding: Dp = 0.dp,
shape: Shape = MaterialTheme.shapes.small,
borderColor: Color = MaterialTheme.colorScheme.secondary,
leadingIcon: @Composable (() -> Unit)? = null,
) {
OutlinedButton(
onClick = onClick,
modifier = modifier.padding(2.dp),
enabled = enabled,
shape = shape,
content = {
if (leadingIcon != null) {
Box(Modifier.sizeIn(maxHeight = ButtonDefaults.IconSize)) { leadingIcon() }
}
Box(
Modifier.padding(
start = when {
leadingIcon != null -> ButtonDefaults.IconSpacing
else -> 0.dp
},
),
) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = if (enabled) {
MaterialTheme.colorScheme.secondary
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
},
modifier = Modifier.padding(textPadding),
)
}
},
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.onSecondary,
),
border = BorderStroke(
width = 1.dp,
color = when {
enabled -> borderColor
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
},
),
)
}
@Composable
public fun OutlinedVerticalIconButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = MaterialTheme.shapes.small,
borderColor: Color = MaterialTheme.colorScheme.secondary,
leadingIcon: @Composable (() -> Unit) = {},
) {
OutlinedButton(
onClick = onClick,
modifier = modifier.widthIn(min = 140.dp),
enabled = enabled,
shape = shape,
content = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
leadingIcon()
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = when {
enabled -> MaterialTheme.colorScheme.secondary
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
},
modifier = Modifier.padding(top = 2.dp),
)
}
},
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.onSecondary,
),
border = BorderStroke(
width = 1.dp,
color = when {
enabled -> borderColor
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
},
),
)
}
@Composable
public fun OutlinedVerticalIconButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = MaterialTheme.shapes.small,
borderColor: Color = MaterialTheme.colorScheme.secondary,
text: @Composable (() -> Unit) = {},
leadingIcon: @Composable (() -> Unit) = {},
) {
OutlinedButton(
onClick = onClick,
modifier = modifier.widthIn(min = 140.dp),
enabled = enabled,
shape = shape,
content = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
leadingIcon()
text()
}
},
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.onSecondary,
),
border = BorderStroke(
width = 1.dp,
color = when {
enabled -> borderColor
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
},
),
)
}
@Composable
public fun ScrimButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
show: Boolean = false,
color: Color = MaterialTheme.colorScheme.surface,
alpha: Float = 0.4f,
content: @Composable () -> Unit,
) {
val isLight = color.luminance() > 0.5
val scrimEnabled = !show
if (scrimEnabled) {
val appTheme = if (isLight) Theme.LIGHT_THEME else Theme.DARK_THEME
TvManiacTheme(appTheme = appTheme) {
IconButton(
onClick = onClick,
modifier = modifier.iconButtonBackgroundScrim(enabled = true, alpha = alpha),
) {
content()
}
}
} else {
IconButton(
onClick = onClick,
modifier = modifier.iconButtonBackgroundScrim(enabled = false, alpha = alpha),
) {
content()
}
}
}
@Composable
public fun RefreshButton(
isRefreshing: Boolean,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Crossfade(isRefreshing, label = "ActionButtonCrossfade") { targetRefreshing ->
if (targetRefreshing) {
AutoSizedCircularProgressIndicator(
modifier = modifier,
)
} else {
content()
}
}
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun FilledTextButtonPreview() {
FilledTextButton(
onClick = {},
enabled = false,
buttonColors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onBackground,
containerColor = MaterialTheme.colorScheme.secondary,
),
modifier = Modifier
.fillMaxWidth()
.padding(2.dp)
.background(color = MaterialTheme.colorScheme.secondary),
) {
Text(
text = "Horror",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondary,
)
}
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun FilledIconButtonPreview(@PreviewParameter(ButtonPreviewParamProvider::class) isEnable: Boolean) {
FilledVerticalIconButton(
onClick = {},
enabled = isEnable,
text = "Track",
imageVector = Icons.Default.LibraryAddCheck,
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun FilledHorizontalIconButtonPreview(@PreviewParameter(ButtonPreviewParamProvider::class) isEnable: Boolean) {
FilledHorizontalIconButton(
onClick = {},
enabled = isEnable,
text = "Add To Library",
imageVector = Icons.Default.LibraryAddCheck,
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun TvManiacAlphaTextButtonPreview() {
FilledTextButton(
onClick = {},
enabled = false,
buttonColors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onSecondary,
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.08f),
),
) {
Text(
text = "Horror",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary,
)
}
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun TvManiacOutlinedButtonPreview() {
OutlinedVerticalIconButton(
onClick = {},
enabled = true,
leadingIcon = {
Image(
imageVector = Icons.Filled.LibraryAddCheck,
contentDescription = null,
colorFilter = ColorFilter.tint(
MaterialTheme.colorScheme.secondary.copy(
alpha = 0.8F,
),
),
)
},
text = "Following",
)
}
private class ButtonPreviewParamProvider : PreviewParameterProvider<Boolean> {
override val values: Sequence<Boolean> = sequenceOf(
true,
false,
)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Card.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmarks
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.i18n.MR.strings.cd_show_poster
@Composable
public fun PosterCard(
imageUrl: String?,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
title: String? = null,
imageWidth: Dp = 120.dp,
aspectRatio: Float = 2 / 3f,
contentScale: ContentScale = ContentScale.Crop,
shape: Shape = RectangleShape,
isInLibrary: Boolean = false,
libraryImageOverlay: ImageVector = Icons.Filled.Bookmarks,
) {
PosterCard(
onClick = onClick,
modifier = modifier,
shape = shape,
imageWidth = imageWidth,
content = {
Box {
PosterPlaceholder(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(aspectRatio)
.align(Alignment.Center),
title = title,
)
AsyncImageComposable(
model = imageUrl,
contentScale = contentScale,
contentDescription = title?.let {
stringResource(
cd_show_poster.resourceId,
title,
)
},
modifier = Modifier
.fillMaxWidth()
.aspectRatio(aspectRatio),
)
if (isInLibrary) {
LibraryOverlay(libraryImageOverlay = libraryImageOverlay)
}
}
},
)
}
@Composable
private fun LibraryOverlay(
libraryImageOverlay: ImageVector,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.TopEnd,
) {
Icon(
imageVector = libraryImageOverlay,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.padding(8.dp)
.size(20.dp),
)
}
}
@Composable
public fun PosterBackdropCard(
title: String,
imageUrl: String?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
textAlign: TextAlign = TextAlign.Start,
contentScale: ContentScale = ContentScale.Crop,
imageWidth: Dp = 120.dp,
aspectRatio: Float = 2 / 3f,
shape: Shape = RectangleShape,
) {
val surface = MaterialTheme.colorScheme.surface
val brush = remember(surface) {
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
surface.copy(alpha = 0.4f),
surface.copy(alpha = 0.7f),
surface.copy(alpha = 0.9f),
surface,
),
)
}
PosterCard(
onClick = onClick,
modifier = modifier,
shape = shape,
imageWidth = imageWidth,
content = {
Box {
PosterPlaceholder(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(aspectRatio)
.align(Alignment.Center),
imageSize = 84.dp,
)
AsyncImageComposable(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(aspectRatio),
model = imageUrl,
contentScale = contentScale,
contentDescription = stringResource(cd_show_poster.resourceId, title),
alignment = Alignment.Center,
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.align(Alignment.BottomCenter)
.background(brush),
)
Text(
text = title,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurface,
textAlign = textAlign,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.align(Alignment.BottomStart),
)
}
},
)
}
@Composable
internal fun PosterCard(
onClick: () -> Unit,
modifier: Modifier = Modifier,
imageWidth: Dp = 120.dp,
shape: Shape = RectangleShape,
content: @Composable () -> Unit,
) {
Card(
onClick = onClick,
modifier = modifier
.width(imageWidth),
shape = shape,
elevation = CardDefaults.cardElevation(
defaultElevation = 4.dp,
),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
content()
}
}
@Composable
public fun CastCard(
profileUrl: String?,
name: String,
characterName: String,
modifier: Modifier = Modifier,
height: Dp = 160.dp,
) {
Card(
modifier = modifier,
shape = MaterialTheme.shapes.small,
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
) {
Box(
modifier = Modifier
.fillMaxSize()
.size(width = 120.dp, height = height),
contentAlignment = Alignment.BottomStart,
) {
CastPlaceholder(
modifier = Modifier.fillMaxSize(),
imageUrl = profileUrl,
name = name,
)
AsyncImageComposable(
model = profileUrl,
contentDescription = name,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
Box(
modifier = Modifier
.matchParentSize()
.background(contentBackgroundGradient()),
)
CastNameOverlay(
name = name,
characterName = characterName,
)
}
}
}
@Composable
private fun CastPlaceholder(
imageUrl: String?,
modifier: Modifier = Modifier,
name: String? = null,
) {
if (imageUrl.isNullOrEmpty()) {
Box(
modifier = modifier
.background(
Brush.verticalGradient(
colors = listOf(
Color.Gray.copy(alpha = 0.8f),
Color.Gray,
),
),
),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(52.dp),
imageVector = Icons.Outlined.Person,
contentDescription = name,
tint = Color.White.copy(alpha = 0.8f),
)
}
}
}
@Composable
private fun CastNameOverlay(
name: String,
characterName: String,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.padding(8.dp)) {
Text(
text = name,
modifier = Modifier
.padding(vertical = 2.dp)
.fillMaxWidth(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
),
)
Text(
text = characterName,
modifier = Modifier.fillMaxWidth(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Normal,
color = MaterialTheme.colorScheme.onSurface,
),
)
}
}
@Composable
private fun contentBackgroundGradient(): Brush {
val surface = MaterialTheme.colorScheme.surface
return remember(surface) {
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
surface.copy(alpha = 0.3f),
surface.copy(alpha = 0.6f),
surface.copy(alpha = 0.9f),
surface,
),
)
}
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun CastCardPreview() {
CastCard(
profileUrl = null,
name = "Tom Hiddleston",
characterName = "Loki",
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun PosterCardPreview() {
PosterCard(
imageUrl = "",
title = "Loki",
modifier = Modifier
.width(100.dp)
.aspectRatio(0.8f),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun PosterCardWithLibraryOverlayPreview() {
PosterCard(
imageUrl = "",
title = "Loki",
isInLibrary = true,
modifier = Modifier
.width(100.dp)
.aspectRatio(0.8f),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun PosterBackdropPreview() {
PosterBackdropCard(
imageUrl = "",
title = "Game of Thrones",
onClick = {},
modifier = Modifier
.fillMaxWidth()
.height(240.dp),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun AvatarComponentPreview() {
AvatarComponent(
imageUrl = "",
size = 38.dp,
contentDescription = "Profile",
onClick = {},
)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Chip.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
@Composable
public fun TvManiacChip(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
selected: Boolean = true,
enabled: Boolean = true,
) {
FilterChip(
modifier = modifier,
selected = selected,
onClick = onClick,
label = {
ProvideTextStyle(value = MaterialTheme.typography.labelSmall) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(vertical = 8.dp),
)
}
},
enabled = enabled,
leadingIcon = null,
border = null,
shape = RoundedCornerShape(4.dp),
colors = FilterChipDefaults.filterChipColors(
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.08f),
labelColor = MaterialTheme.colorScheme.secondary,
selectedContainerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.24f),
selectedLabelColor = MaterialTheme.colorScheme.secondary,
),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun ChipItemSelectedPreview() {
TvManiacChip(
selected = true,
text = "Season 1",
onClick = {},
)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Dialogs.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.thomaskioko.tvmaniac.compose.components.ThemePreviews
import com.thomaskioko.tvmaniac.compose.components.TvManiacPreviewWrapperProvider
@Composable
public fun TvManiacAlertDialog(
title: String,
message: String,
confirmButtonText: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape = MaterialTheme.shapes.small,
icon: ImageVector? = null,
dismissButtonText: String? = null,
confirmButtonTestTag: String? = null,
dismissButtonTestTag: String? = null,
) {
val density = LocalDensity.current
val containerWidth = with(density) {
LocalWindowInfo.current.containerSize.width.toDp()
}
AlertDialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
modifier = Modifier.widthIn(max = (containerWidth - 80.dp).coerceAtLeast(0.dp)),
shape = shape,
onDismissRequest = onDismiss,
icon = icon?.let {
{
Icon(
imageVector = it,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
)
}
},
title = {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
},
text = {
Text(
text = message,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
},
confirmButton = {
TextButton(
modifier = confirmButtonTestTag?.let { Modifier.testTag(it) } ?: Modifier,
onClick = onConfirm,
) {
Text(
text = confirmButtonText,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.secondary,
)
}
},
dismissButton = dismissButtonText?.let {
{
TextButton(
modifier = dismissButtonTestTag?.let { tag -> Modifier.testTag(tag) } ?: Modifier,
onClick = onDismiss,
) {
Text(
text = it,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
},
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun TvManiacAlertDialogPreview() {
TvManiacAlertDialog(
title = "Enable Notifications",
message = "Get notified when new episodes of your favorite shows are released.",
confirmButtonText = "Enable",
dismissButtonText = "Not Now",
icon = Icons.Default.Info,
onConfirm = {},
onDismiss = {},
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun TvManiacAlertDialogNoIconPreview() {
TvManiacAlertDialog(
title = "Confirm Action",
message = "Are you sure you want to proceed with this action?",
confirmButtonText = "Confirm",
dismissButtonText = "Cancel",
onConfirm = {},
onDismiss = {},
)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/EmptyLayout.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Inbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.compose.components.ThemePreviews
import com.thomaskioko.tvmaniac.compose.components.TvManiacPreviewWrapperProvider
@Composable
public fun EmptyStateView(
title: String,
modifier: Modifier = Modifier,
imageVector: ImageVector = Icons.Outlined.Inbox,
message: String? = null,
buttonText: String? = null,
buttonTestTag: String? = null,
onClick: () -> Unit = {},
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Icon(
modifier = Modifier.size(64.dp),
imageVector = imageVector,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
contentDescription = null,
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
)
message?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
buttonText?.let {
Spacer(modifier = Modifier.height(24.dp))
HorizontalOutlinedButton(
modifier = buttonTestTag?.let { Modifier.testTag(it) } ?: Modifier,
text = it,
onClick = onClick,
shape = MaterialTheme.shapes.small,
)
}
}
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun EmptyStateViewPreview() {
EmptyStateView(
title = "Nothing here yet",
message = "Shows you follow will appear here.",
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun EmptyStateViewWithButtonPreview() {
EmptyStateView(
title = "Something went wrong",
message = "We couldn't load the data.",
buttonText = "Retry",
onClick = {},
)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/ErrorLayout.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SignalWifi4Bar
import androidx.compose.material.icons.outlined.SignalWifiOff
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.compose.components.ThemePreviews
import com.thomaskioko.tvmaniac.compose.components.TvManiacPreviewWrapperProvider
import com.thomaskioko.tvmaniac.compose.theme.green
import com.thomaskioko.tvmaniac.i18n.MR.strings.cd_connectivity_icon
import com.thomaskioko.tvmaniac.i18n.MR.strings.status_connected
import com.thomaskioko.tvmaniac.i18n.MR.strings.status_no_connection
import com.thomaskioko.tvmaniac.i18n.MR.strings.unexpected_error_retry
import com.thomaskioko.tvmaniac.i18n.resolve
@Composable
public fun ConnectionStatus(
isConnected: Boolean,
modifier: Modifier = Modifier,
) {
val backgroundColor by
animateColorAsState(
if (isConnected) green else MaterialTheme.colorScheme.error,
label = "",
)
val message = if (isConnected) {
status_connected.resolve(LocalContext.current)
} else {
status_no_connection.resolve(LocalContext.current)
}
val icon = if (isConnected) Icons.Outlined.SignalWifi4Bar else Icons.Outlined.SignalWifiOff
Box(
modifier = modifier
.background(backgroundColor)
.fillMaxWidth()
.padding(8.dp),
contentAlignment = Alignment.TopCenter,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Icon(
imageVector = icon,
contentDescription = cd_connectivity_icon.resolve(LocalContext.current),
tint = Color.White,
)
Spacer(modifier = Modifier.size(8.dp))
Text(
message,
color = Color.White,
style = MaterialTheme.typography.labelMedium,
)
}
}
}
@Composable
public fun RowError(
onRetry: () -> Unit,
modifier: Modifier = Modifier,
errorMessage: String = unexpected_error_retry.resolve(LocalContext.current),
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
text = errorMessage,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(8.dp))
HorizontalOutlinedButton(
text = "Retry",
onClick = onRetry,
)
}
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun RowErrorPreview() {
RowError(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
onRetry = {},
)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/FilterChipSection.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.compose.components.ThemePreviews
import com.thomaskioko.tvmaniac.compose.components.TvManiacPreviewWrapperProvider
import com.thomaskioko.tvmaniac.i18n.MR.strings.label_library_filter_show_less
import com.thomaskioko.tvmaniac.i18n.MR.strings.label_library_filter_show_more
import com.thomaskioko.tvmaniac.i18n.resolve
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
@OptIn(ExperimentalLayoutApi::class)
@Composable
public fun <T> FilterChipSection(
title: String,
items: ImmutableList<T>,
selectedItems: ImmutableSet<T>,
onItemToggle: (T) -> Unit,
labelProvider: (T) -> String,
modifier: Modifier = Modifier,
collapsedItemCount: Int = 5,
singleSelect: Boolean = false,
) {
val context = LocalContext.current
var isExpanded by remember { mutableStateOf(false) }
val visibleItems = if (isExpanded) items else items.take(collapsedItemCount)
val hasMoreItems = items.size > collapsedItemCount
Column(
modifier = modifier
.fillMaxWidth()
.animateContentSize(),
) {
SectionHeader(title = title)
Spacer(modifier = Modifier.height(12.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
visibleItems.forEach { item ->
val isSelected = item in selectedItems
SelectableFilterChip(
label = labelProvider(item),
isSelected = isSelected,
onClick = { onItemToggle(item) },
)
}
}
if (hasMoreItems) {
Spacer(modifier = Modifier.height(8.dp))
ShowMoreToggle(
isExpanded = isExpanded,
showMoreText = label_library_filter_show_more.resolve(context),
showLessText = label_library_filter_show_less.resolve(context),
onToggle = { isExpanded = !isExpanded },
)
}
}
}
@Composable
public fun SectionHeader(
title: String,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
HorizontalDivider(
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.outlineVariant,
)
Text(
text = title,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp),
)
HorizontalDivider(
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.outlineVariant,
)
}
}
@Composable
public fun SelectableFilterChip(
label: String,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
FilterChip(
modifier = modifier,
selected = isSelected,
onClick = onClick,
label = {
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
)
},
shape = RoundedCornerShape(8.dp),
colors = FilterChipDefaults.filterChipColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
selectedContainerColor = MaterialTheme.colorScheme.secondary,
labelColor = MaterialTheme.colorScheme.onSurface,
selectedLabelColor = MaterialTheme.colorScheme.onSecondary,
),
border = FilterChipDefaults.filterChipBorder(
borderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
selectedBorderColor = Color.Transparent,
enabled = true,
selected = isSelected,
),
)
}
@Composable
internal fun ShowMoreToggle(
isExpanded: Boolean,
showMoreText: String,
showLessText: String,
onToggle: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.clickable { onToggle() }
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = if (isExpanded) showLessText else showMoreText,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Icon(
imageVector = if (isExpanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun FilterChipSectionPreview() {
FilterChipSection(
title = "GENRES",
items = persistentListOf(
"Action & Adventure",
"Animation",
"Comedy",
"Crime",
"Drama",
"Fantasy",
"Sci-Fi",
),
selectedItems = persistentSetOf("Drama", "Comedy"),
onItemToggle = {},
labelProvider = { it },
modifier = Modifier.padding(16.dp),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun FilterChipSectionCollapsedPreview() {
FilterChipSection(
title = "STATUS",
items = persistentListOf(
"Returning Series",
"Planned",
"In Production",
"Ended",
"Canceled",
),
selectedItems = persistentSetOf("Returning Series"),
onItemToggle = {},
labelProvider = { it },
collapsedItemCount = 3,
modifier = Modifier.padding(16.dp),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun SectionHeaderPreview() {
SectionHeader(
title = "SORT BY",
modifier = Modifier.padding(16.dp),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun SelectableFilterChipPreview() {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(16.dp),
) {
SelectableFilterChip(
label = "Last watched ↓",
isSelected = true,
onClick = {},
)
SelectableFilterChip(
label = "Alphabetical",
isSelected = false,
onClick = {},
)
}
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/GradientScrim.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import android.annotation.SuppressLint
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import kotlin.math.pow
/**
* Draws a vertical gradient scrim in the foreground.
*
* @param color The color of the gradient scrim.
* @param decay The exponential decay to apply to the gradient. Defaults to `3.0f` which is
* a cubic decay.
* @param numStops The number of color stops to draw in the gradient. Higher numbers result in
* the higher visual quality at the cost of draw performance. Defaults to `16`.
*/
@SuppressLint("ComposeModifierComposed")
public fun Modifier.drawForegroundGradientScrim(
color: Color,
decay: Float = 3.0f,
numStops: Int = 16,
startY: Float = 0f,
endY: Float = 1f,
): Modifier = composed {
val colors = remember(color, numStops) {
val baseAlpha = color.alpha
List(numStops) { i ->
val x = i * 1f / (numStops - 1)
val opacity = x.pow(decay)
color.copy(alpha = baseAlpha * opacity)
}
}
drawWithContent {
drawContent()
drawRect(
topLeft = Offset(x = 0f, y = startY * size.height),
size = size.copy(height = (endY - startY) * size.height),
brush = Brush.verticalGradient(colors = colors),
)
}
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Image.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import androidx.compose.ui.util.lerp
import androidx.compose.ui.viewinterop.AndroidView
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.load
import coil.request.ImageRequest
import com.flaviofaria.kenburnsview.KenBurnsView
import com.thomaskioko.tvmaniac.compose.components.ThemePreviews
import com.thomaskioko.tvmaniac.compose.components.TvManiacPreviewWrapperProvider
import kotlin.math.absoluteValue
@Composable
public fun AsyncImageComposable(
model: Any?,
contentDescription: String?,
modifier: Modifier = Modifier,
shape: Shape = RectangleShape,
border: BorderStroke? = null,
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,
) {
AsyncImage(
model = requestBuilder?.let { builder ->
when (model) {
is ImageRequest -> model.newBuilder()
else -> ImageRequest.Builder(LocalContext.current).data(model)
}
.builder()
.build()
} ?: model,
contentDescription = contentDescription,
modifier = modifier
.clip(shape)
.then(if (border != null) Modifier.border(border, shape) else Modifier),
transform = transform,
onState = onState,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality,
)
}
@Composable
public fun AvatarComponent(
imageUrl: String?,
size: Dp,
modifier: Modifier = Modifier,
contentDescription: String? = null,
border: BorderStroke? = null,
placeholderIcon: ImageVector = Icons.Outlined.Person,
onClick: (() -> Unit)? = null,
) {
val commonModifier = modifier
.size(size)
.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier)
if (imageUrl.isNullOrEmpty()) {
Box(
modifier = commonModifier
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = CircleShape,
)
.then(if (border != null) Modifier.border(border, CircleShape) else Modifier),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = placeholderIcon,
contentDescription = contentDescription,
modifier = Modifier.size(size * 0.6f),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} else {
AsyncImageComposable(
model = imageUrl,
contentDescription = contentDescription,
modifier = commonModifier,
shape = CircleShape,
border = border,
contentScale = ContentScale.Crop,
)
}
}
@Composable
public fun KenBurnsViewImage(
imageUrl: String?,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val kenBuns = remember { KenBurnsView(context) }
Box(modifier = modifier) {
PosterPlaceholder(modifier = Modifier.fillMaxSize())
AndroidView(
factory = { kenBuns },
modifier = Modifier.fillMaxSize(),
) { it.load(imageUrl) }
}
}
@Composable
public fun ParallaxCarouselImage(
state: PagerState,
currentPage: Int,
imageUrl: String?,
modifier: Modifier = Modifier,
shape: Shape = RectangleShape,
overlayContent: @Composable () -> Unit = {},
) {
val currentPageOffset = calculatePageOffset(state, currentPage)
val cardTranslationX = lerp(100f, 0f, 1f - currentPageOffset)
val cardScaleX = lerp(0.8f, 1f, 1f - currentPageOffset.absoluteValue.coerceIn(0f, 1f))
val density = LocalDensity.current
val screenWidth = with(density) {
LocalWindowInfo.current.containerSize.width.toDp()
}
val parallaxOffset = currentPageOffset * screenWidth * 2f
Box(
modifier = modifier
.fillMaxWidth()
.graphicsLayer {
scaleX = cardScaleX
translationX = cardTranslationX
},
) {
PosterPlaceholder(
modifier = Modifier
.fillMaxSize()
.clip(shape),
)
AsyncImageComposable(
modifier = Modifier
.fillMaxSize()
.clip(shape)
.graphicsLayer {
translationX = lerp(10f, 0f, 1f - currentPageOffset) + parallaxOffset.value
},
model = imageUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
)
overlayContent()
}
}
private fun calculatePageOffset(state: PagerState, currentPage: Int): Float {
return (state.currentPage + state.currentPageOffsetFraction - currentPage).coerceIn(-1f, 1f)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun ParallaxCarouselImagePreview() {
val pagerState = rememberPagerState(pageCount = { 3 })
ParallaxCarouselImage(
state = pagerState,
currentPage = 0,
imageUrl = null,
modifier = Modifier
.fillMaxWidth()
.height(360.dp),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun KenBurnsViewImagePreview() {
KenBurnsViewImage(
imageUrl = null,
modifier = Modifier
.fillMaxWidth()
.height(240.dp),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun AvatarComponentPreview() {
AvatarComponent(
imageUrl = "https://image.png",
size = 64.dp,
modifier = Modifier.padding(16.dp),
border = BorderStroke(2.dp, MaterialTheme.colorScheme.primary),
)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/NavigationBar.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Movie
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.VideoLibrary
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.compose.components.ThemePreviews
import com.thomaskioko.tvmaniac.compose.components.TvManiacPreviewWrapperProvider
import com.thomaskioko.tvmaniac.i18n.MR.strings.menu_item_discover
import com.thomaskioko.tvmaniac.i18n.MR.strings.menu_item_library
import com.thomaskioko.tvmaniac.i18n.MR.strings.menu_item_search
import com.thomaskioko.tvmaniac.i18n.MR.strings.menu_item_settings
import com.thomaskioko.tvmaniac.i18n.resolve
@Composable
public fun TvManiacNavigationBar(
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit,
) {
NavigationBar(
modifier = modifier,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
contentColor = NavigationDefaultColors.navigationContentColor(),
tonalElevation = 8.dp,
content = content,
)
}
@Composable
public fun RowScope.TvManiacBottomNavigationItem(
imageVector: ImageVector,
title: String,
selected: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
NavigationBarItem(
modifier = modifier,
icon = {
Icon(
imageVector = imageVector,
contentDescription = title,
)
},
label = { Text(title) },
selected = selected,
alwaysShowLabel = true,
colors = NavigationBarItemDefaults.colors(
selectedIconColor = NavigationDefaultColors.navigationSelectedItemColor(),
unselectedIconColor = NavigationDefaultColors.navigationContentColor(),
selectedTextColor = NavigationDefaultColors.navigationSelectedItemColor(),
unselectedTextColor = NavigationDefaultColors.navigationContentColor(),
indicatorColor = Color.Transparent,
),
onClick = onClick,
)
}
public object NavigationDefaultColors {
@Composable
public fun navigationContentColor(): Color = MaterialTheme.colorScheme.onSurfaceVariant
@Composable
public fun navigationSelectedItemColor(): Color = MaterialTheme.colorScheme.secondary
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun TvManiacTvManiacNavigationBarPreviewPreview() {
TvManiacNavigationBar {
TvManiacBottomNavigationItem(
imageVector = Icons.Outlined.Movie,
title = menu_item_discover.resolve(LocalContext.current),
selected = true,
onClick = { },
)
TvManiacBottomNavigationItem(
imageVector = Icons.Outlined.Search,
title = menu_item_search.resolve(LocalContext.current),
selected = false,
onClick = { },
)
TvManiacBottomNavigationItem(
imageVector = Icons.Outlined.VideoLibrary,
title = menu_item_library.resolve(LocalContext.current),
selected = false,
onClick = { },
)
TvManiacBottomNavigationItem(
imageVector = Icons.Outlined.Settings,
title = menu_item_settings.resolve(LocalContext.current),
selected = false,
onClick = { },
)
}
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/NotificationRationaleContent.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.compose.components.ThemePreviews
import com.thomaskioko.tvmaniac.compose.components.TvManiacPreviewWrapperProvider
import com.thomaskioko.tvmaniac.i18n.MR.strings.notification_rationale_enable
import com.thomaskioko.tvmaniac.i18n.MR.strings.notification_rationale_message
import com.thomaskioko.tvmaniac.i18n.MR.strings.notification_rationale_not_now
import com.thomaskioko.tvmaniac.i18n.MR.strings.notification_rationale_title
import com.thomaskioko.tvmaniac.testtags.notifications.NotificationRationaleTestTags
import dev.icerock.moko.resources.compose.stringResource
@Composable
public fun NotificationRationaleContent(
onEnable: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.testTag(NotificationRationaleTestTags.BOTTOM_SHEET)
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = Icons.Default.Notifications,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.secondary,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(notification_rationale_title),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(notification_rationale_message),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(16.dp))
EpisodeDateSection()
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = onEnable,
modifier = Modifier
.fillMaxWidth()
.testTag(NotificationRationaleTestTags.ENABLE_BUTTON),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary,
contentColor = MaterialTheme.colorScheme.onSecondary,
),
shape = MaterialTheme.shapes.small,
) {
Text(text = stringResource(notification_rationale_enable))
}
TextButton(
onClick = onDismiss,
modifier = Modifier
.fillMaxWidth()
.testTag(NotificationRationaleTestTags.DISMISS_BUTTON),
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.secondary,
),
) {
Text(text = stringResource(notification_rationale_not_now))
}
Spacer(modifier = Modifier.height(16.dp))
}
}
@Composable
private fun EpisodeDateSection() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth(),
) {
GradientDivider()
Spacer(modifier = Modifier.height(12.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
listOf(12, 13, 14).forEach { day ->
Text(
text = "$day",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.size(56.dp)
.border(
width = 2.dp,
color = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(8.dp),
),
) {
Text(
text = "15",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "FEB",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
listOf(16, 17, 18).forEach { day ->
Text(
text = "$day",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
Spacer(modifier = Modifier.height(12.dp))
GradientDivider()
}
}
@Composable
private fun GradientDivider() {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.height(1.dp)
.background(
Brush.horizontalGradient(
colors = listOf(
Color.Transparent,
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f),
Color.Transparent,
),
),
),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun NotificationRationaleContentPreview() {
NotificationRationaleContent(
onEnable = {},
onDismiss = {},
)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/PosterPlaceholder.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Movie
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.thomaskioko.tvmaniac.compose.components.ThemePreviews
import com.thomaskioko.tvmaniac.compose.components.TvManiacPreviewWrapperProvider
@Composable
public fun PosterPlaceholder(
modifier: Modifier = Modifier,
imageSize: Dp = 52.dp,
title: String? = null,
) {
val brush = remember {
Brush.verticalGradient(
colors = listOf(
Color.Gray.copy(alpha = 0.8f),
Color.Gray,
),
)
}
ConstraintLayout(
modifier = modifier
.fillMaxSize()
.background(brush),
) {
val (icon, text) = createRefs()
Icon(
modifier = Modifier
.size(imageSize)
.constrainAs(icon) {
centerTo(parent)
},
imageVector = Icons.Outlined.Movie,
contentDescription = title,
tint = Color.White.copy(alpha = 0.8f),
)
title?.let {
Text(
text = it,
modifier = Modifier
.padding(horizontal = 4.dp)
.constrainAs(text) {
top.linkTo(icon.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
width = Dimension.fillToConstraints
},
style = MaterialTheme.typography.bodyMedium,
color = Color.White.copy(alpha = 0.8f),
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun PosterPlaceholderPreview() {
PosterPlaceholder(
title = "Loki",
modifier = Modifier
.width(120.dp)
.aspectRatio(2 / 3f),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun PosterPlaceholderNoTitlePreview() {
PosterPlaceholder(
modifier = Modifier
.width(120.dp)
.aspectRatio(2 / 3f),
)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/ProgressIndicator.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
public fun LoadingIndicator(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.secondary,
strokeWidth: Dp = 4.dp,
) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
color = color,
strokeWidth = strokeWidth,
)
}
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun CircularProgressIndicatorPreview() {
CircularProgressIndicator()
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/ScanlineOverlay.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.domain.theme.Theme
public data class ScanlineConfiguration(
val enabled: Boolean,
val color: Color,
val lineHeight: Dp = 2.dp,
val opacity: Float = 0.15f,
) {
internal companion object {
internal val Disabled: ScanlineConfiguration =
ScanlineConfiguration(enabled = false, color = Color.Transparent)
internal fun terminal(): ScanlineConfiguration = ScanlineConfiguration(
enabled = true,
color = Color(0xFF20C020),
opacity = 0.12f,
)
internal fun amber(): ScanlineConfiguration = ScanlineConfiguration(
enabled = true,
color = Color(0xFFFF8C00),
opacity = 0.12f,
)
internal fun snow(): ScanlineConfiguration = ScanlineConfiguration(
enabled = true,
color = Color(0xFFFFFFFF),
opacity = 0.08f,
)
internal fun crimson(): ScanlineConfiguration = ScanlineConfiguration(
enabled = true,
color = Color(0xFFFF4D6A),
opacity = 0.12f,
)
}
}
internal fun Theme.toScanlineConfiguration(): ScanlineConfiguration = when (this) {
Theme.TERMINAL_THEME -> ScanlineConfiguration.terminal()
Theme.AMBER_THEME -> ScanlineConfiguration.amber()
Theme.SNOW_THEME -> ScanlineConfiguration.snow()
Theme.CRIMSON_THEME -> ScanlineConfiguration.crimson()
else -> ScanlineConfiguration.Disabled
}
@Composable
public fun ScanlineOverlay(
configuration: ScanlineConfiguration,
modifier: Modifier = Modifier,
) {
if (!configuration.enabled) return
val lineColor = configuration.color.copy(alpha = configuration.opacity)
Canvas(modifier = modifier.fillMaxSize()) {
val lineHeightPx = configuration.lineHeight.toPx()
val lineSpacing = lineHeightPx * 2
var y = 0f
while (y < size.height) {
drawRect(
color = lineColor,
topLeft = Offset(0f, y),
size = Size(size.width, lineHeightPx),
)
y += lineSpacing
}
}
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/SearchTextField.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.compose.components.ThemePreviews
import com.thomaskioko.tvmaniac.compose.components.TvManiacPreviewWrapperProvider
import com.thomaskioko.tvmaniac.i18n.MR.strings.cd_clear_text
import com.thomaskioko.tvmaniac.i18n.resolve
import kotlinx.coroutines.launch
@Composable
public fun SearchTextContainer(
query: String,
hint: String,
lazyListState: LazyListState,
onQueryChanged: (String) -> Unit,
onClearQuery: () -> Unit,
modifier: Modifier = Modifier,
textFieldModifier: Modifier = Modifier,
keyboardType: KeyboardType = KeyboardType.Text,
content: @Composable () -> Unit,
) {
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
val coroutineScope = rememberCoroutineScope()
val textState = remember(query) {
mutableStateOf(TextFieldValue(query, TextRange(query.length)))
}
val hasFocus = remember { mutableStateOf(false) }
LaunchedEffect(lazyListState) {
snapshotFlow { lazyListState.isScrollInProgress }
.collect { isScrolling ->
if (isScrolling) {
keyboardController?.hide()
focusManager.clearFocus()
}
}
}
SearchTextFieldContent(
modifier = modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = {
keyboardController?.hide()
focusManager.clearFocus()
},
)
},
textFieldModifier = textFieldModifier,
textFieldValue = textState.value,
hint = hint,
keyboardType = keyboardType,
onTextChanged = { newValue ->
textState.value = newValue
onQueryChanged(newValue.text)
},
onFocusChanged = { hasFocus.value = it },
onClearClick = {
textState.value = TextFieldValue()
onClearQuery()
keyboardController?.hide()
focusManager.clearFocus()
},
onSubmit = {
coroutineScope.launch {
onQueryChanged(textState.value.text)
keyboardController?.hide()
focusManager.clearFocus()
}
},
content = content,
)
}
@Composable
private fun SearchTextFieldContent(
textFieldValue: TextFieldValue,
hint: String,
keyboardType: KeyboardType,
onTextChanged: (TextFieldValue) -> Unit,
onFocusChanged: (Boolean) -> Unit,
onClearClick: () -> Unit,
onSubmit: () -> Unit,
modifier: Modifier = Modifier,
textFieldModifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Column(modifier = modifier) {
SearchTextField(
modifier = textFieldModifier
.padding(horizontal = 16.dp),
onFocusChanged = onFocusChanged,
textFieldValue = textFieldValue,
onTextChanged = onTextChanged,
hint = hint,
keyboardType = keyboardType,
onSubmit = onSubmit,
onClearClick = onClearClick,
)
content()
}
}
@Composable
private fun SearchTextField(
onFocusChanged: (Boolean) -> Unit,
textFieldValue: TextFieldValue,
onTextChanged: (TextFieldValue) -> Unit,
hint: String,
keyboardType: KeyboardType,
onSubmit: () -> Unit,
onClearClick: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape = MaterialTheme.shapes.medium,
) {
OutlinedTextField(
modifier = modifier
.fillMaxWidth()
.onFocusChanged { onFocusChanged(it.isFocused) },
value = textFieldValue,
onValueChange = onTextChanged,
placeholder = {
Text(
text = hint,
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
),
)
},
singleLine = true,
maxLines = 1,
textStyle = MaterialTheme.typography.bodyMedium,
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType,
imeAction = ImeAction.Search,
),
keyboardActions = KeyboardActions(
onSearch = { onSubmit() },
),
leadingIcon = {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
)
},
trailingIcon = {
IconButton(onClick = onClearClick) {
Icon(
imageVector = Icons.Filled.Clear,
contentDescription = cd_clear_text.resolve(LocalContext.current),
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
)
}
},
shape = shape,
colors = TextFieldDefaults.colors(
focusedIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
unfocusedIndicatorColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
cursorColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun SearchTextFieldPreview() {
SearchTextContainer(
hint = "Enter Show Title",
query = "",
lazyListState = remember { LazyListState() },
onClearQuery = {},
onQueryChanged = {},
content = {},
)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/SegmentedProgressBar.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.compose.components.ThemePreviews
import com.thomaskioko.tvmaniac.compose.components.TvManiacPreviewWrapperProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Composable
public fun SegmentedProgressBar(
segmentProgress: ImmutableList<Float>,
modifier: Modifier = Modifier,
height: Dp = 6.dp,
segmentGap: Dp = 4.dp,
trackColor: Color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f),
) {
if (segmentProgress.isEmpty()) return
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(segmentGap),
) {
segmentProgress.forEach { progress ->
ProgressSegment(
progress = progress,
modifier = Modifier.weight(1f),
height = height,
trackColor = trackColor,
)
}
}
}
@Composable
private fun ProgressSegment(
progress: Float,
modifier: Modifier = Modifier,
height: Dp = 6.dp,
trackColor: Color,
) {
val shape = RoundedCornerShape(height / 2)
val progressColor = MaterialTheme.colorScheme.secondary
Box(
modifier = modifier
.height(height)
.clip(shape)
.background(trackColor),
) {
Box(
modifier = Modifier
.fillMaxWidth(fraction = progress.coerceIn(0f, 1f))
.height(height)
.clip(shape)
.background(progressColor),
)
}
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun SegmentedProgressBarPreview() {
SegmentedProgressBar(
segmentProgress = persistentListOf(1f, 0.5f, 0f),
modifier = Modifier.fillMaxWidth(),
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun SegmentedProgressBarSinglePreview() {
SegmentedProgressBar(
segmentProgress = persistentListOf(0.75f),
modifier = Modifier.fillMaxWidth(),
)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/SheetDragHandle.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.KeyboardArrowDown
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.compose.components.ThemePreviews
import com.thomaskioko.tvmaniac.compose.components.TvManiacPreviewWrapperProvider
import com.thomaskioko.tvmaniac.i18n.MR.strings.cd_expand_collapse
import com.thomaskioko.tvmaniac.i18n.resolve
@Composable
public fun SheetDragHandle(
onClick: () -> Unit,
imageVector: ImageVector,
modifier: Modifier = Modifier,
title: String? = null,
textAlign: TextAlign? = null,
tint: Color = LocalContentColor.current,
) {
val context = LocalContext.current
Box(
modifier = modifier
.fillMaxWidth()
.statusBarsPadding()
.height(56.dp)
.background(Color.Transparent),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterStart)
.padding(start = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = imageVector,
tint = tint,
contentDescription = cd_expand_collapse.resolve(context),
modifier = Modifier
.size(24.dp)
.clickable { onClick() },
)
Spacer(modifier = Modifier.width(8.dp))
title?.let {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = textAlign,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun CustomSheetDragHandlePreview() {
SheetDragHandle(
title = "Drag Handle",
onClick = {},
imageVector = Icons.Outlined.KeyboardArrowDown,
)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/ShowLinearProgressIndicator.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import com.thomaskioko.tvmaniac.compose.theme.green
@Composable
public fun ShowLinearProgressIndicator(
progress: Float,
modifier: Modifier = Modifier,
) {
LinearProgressIndicator(
progress = { progress },
color = MaterialTheme.colorScheme.secondary,
trackColor = if (progress == 1f) {
green.copy(alpha = 0.5F)
} else {
MaterialTheme.colorScheme.secondary.copy(
alpha = 0.5F,
)
},
strokeCap = StrokeCap.Butt,
drawStopIndicator = {},
gapSize = 0.dp,
modifier = modifier,
)
}
@ThemePreviews
@PreviewWrapper(TvManiacPreviewWrapperProvider::class)
@Composable
private fun ShowLinearProgressIndicatorPreview() {
ShowLinearProgressIndicator(progress = 0.6f)
}
================================================
FILE: android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Snackbar.kt
================================================
package com.thomaskioko.tvmaniac.compose.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androi
Showing preview only (273K chars total). Download the full file or copy to clipboard to get everything.
gitextract_xytv9ppo/ ├── .editorconfig ├── .geminiignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── actions/ │ │ ├── setup-android-release/ │ │ │ └── action.yml │ │ ├── setup-gradle/ │ │ │ └── action.yml │ │ ├── setup-ios/ │ │ │ └── action.yml │ │ └── setup-ios-release/ │ │ └── action.yml │ ├── release.yml │ ├── renovate.json │ └── workflows/ │ ├── baseline-profile.yml │ ├── beta-release.yml │ ├── ci.yml │ ├── compare-screenshot.yml │ ├── daily-build.yml │ ├── nightly-integration-tests.yml │ ├── promote-release.yml │ ├── release.yml │ └── store-screenshot.yml ├── .gitignore ├── .idea/ │ ├── codeStyles/ │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ └── dictionaries/ │ └── project.xml ├── .ruby-version ├── .swiftformat ├── .swiftlint.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── GEMINI.md ├── Gemfile ├── LICENSE ├── README.md ├── android-designsystem/ │ ├── build.gradle.kts │ └── src/ │ ├── debug/ │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ ├── main/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── compose/ │ │ │ ├── components/ │ │ │ │ ├── Background.kt │ │ │ │ ├── BadgeChip.kt │ │ │ │ ├── Buttons.kt │ │ │ │ ├── Card.kt │ │ │ │ ├── Chip.kt │ │ │ │ ├── Dialogs.kt │ │ │ │ ├── EmptyLayout.kt │ │ │ │ ├── ErrorLayout.kt │ │ │ │ ├── FilterChipSection.kt │ │ │ │ ├── GradientScrim.kt │ │ │ │ ├── Image.kt │ │ │ │ ├── NavigationBar.kt │ │ │ │ ├── NotificationRationaleContent.kt │ │ │ │ ├── PosterPlaceholder.kt │ │ │ │ ├── ProgressIndicator.kt │ │ │ │ ├── ScanlineOverlay.kt │ │ │ │ ├── SearchTextField.kt │ │ │ │ ├── SegmentedProgressBar.kt │ │ │ │ ├── SheetDragHandle.kt │ │ │ │ ├── ShowLinearProgressIndicator.kt │ │ │ │ ├── Snackbar.kt │ │ │ │ ├── Text.kt │ │ │ │ ├── TextTitlePill.kt │ │ │ │ ├── TopBar.kt │ │ │ │ ├── TvManiacBottomSheet.kt │ │ │ │ └── TvManiacPreviewWrapperProvider.kt │ │ │ ├── extensions/ │ │ │ │ ├── GradientExtensions.kt │ │ │ │ ├── LazyListExtensions.kt │ │ │ │ ├── PaddingValuesExtentions.kt │ │ │ │ └── ScrimExtentions.kt │ │ │ ├── theme/ │ │ │ │ ├── Background.kt │ │ │ │ ├── Colors.kt │ │ │ │ ├── Shape.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── util/ │ │ │ ├── AutoAdvanceLocal.kt │ │ │ └── DynamicTheming.kt │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ └── test/ │ └── kotlin/ │ └── com/ │ └── thomaskioko/ │ └── tvmaniac/ │ └── compose/ │ └── roborazzi/ │ ├── NotificationRationaleContentScreenshotTest.kt │ └── TvManiacSnackBarScreenshotTest.kt ├── api/ │ ├── tmdb/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── tmdb/ │ │ │ └── api/ │ │ │ ├── TmdbConfig.kt │ │ │ ├── TmdbSeasonDetailsNetworkDataSource.kt │ │ │ ├── TmdbShowDetailsNetworkDataSource.kt │ │ │ ├── TmdbShowsNetworkDataSource.kt │ │ │ └── model/ │ │ │ ├── CreditsResponse.kt │ │ │ ├── EpisodesResponse.kt │ │ │ ├── GenreResponse.kt │ │ │ ├── ImagesResponse.kt │ │ │ ├── LastEpisodeToAirResponse.kt │ │ │ ├── NetworksResponse.kt │ │ │ ├── NextEpisodeToAirResponse.kt │ │ │ ├── SeasonsResponse.kt │ │ │ ├── TmdbGenreResult.kt │ │ │ ├── TmdbSeasonDetailsResponse.kt │ │ │ ├── TmdbShowDetailsResponse.kt │ │ │ ├── TmdbShowResponse.kt │ │ │ ├── VideosResponse.kt │ │ │ └── WatchProvidersResult.kt │ │ └── implementation/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── androidMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── tmdb/ │ │ │ └── implementation/ │ │ │ └── TmdbPlatformBindingContainer.kt │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── tmdb/ │ │ │ └── implementation/ │ │ │ ├── DefaultTmdbSeasonDetailsNetworkDataSource.kt │ │ │ ├── DefaultTmdbShowDetailsNetworkDataSource.kt │ │ │ ├── DefaultTmdbShowsNetworkDataSource.kt │ │ │ ├── TmdbBindingContainer.kt │ │ │ └── TmdbClient.kt │ │ └── iosMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── tmdb/ │ │ └── implementation/ │ │ └── TmdbPlatformBindingContainer.kt │ └── trakt/ │ ├── api/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── trakt/ │ │ └── api/ │ │ ├── TraktCalendarRemoteDataSource.kt │ │ ├── TraktConfig.kt │ │ ├── TraktEpisodeHistoryRemoteDataSource.kt │ │ ├── TraktListRemoteDataSource.kt │ │ ├── TraktShowsRemoteDataSource.kt │ │ ├── TraktSyncRemoteDataSource.kt │ │ ├── TraktTokenRemoteDataSource.kt │ │ ├── TraktUserRemoteDataSource.kt │ │ └── model/ │ │ ├── AccessTokenBody.kt │ │ ├── RefreshAccessTokenBody.kt │ │ ├── TraktAccessRefreshTokenResponse.kt │ │ ├── TraktAccessTokenResponse.kt │ │ ├── TraktAddShowRequest.kt │ │ ├── TraktAddShowToListResponse.kt │ │ ├── TraktCalendarResponse.kt │ │ ├── TraktCreateListRequest.kt │ │ ├── TraktCreateListResponse.kt │ │ ├── TraktFollowedShowResponse.kt │ │ ├── TraktGenreResponse.kt │ │ ├── TraktLastActivitiesResponse.kt │ │ ├── TraktNextEpisodeResponse.kt │ │ ├── TraktPeopleResponse.kt │ │ ├── TraktPersonalListsResponse.kt │ │ ├── TraktRemoveShowFromListResponse.kt │ │ ├── TraktSeasonEpisodesResponse.kt │ │ ├── TraktSeasonsResponse.kt │ │ ├── TraktShowsResponse.kt │ │ ├── TraktSyncModels.kt │ │ ├── TraktUserResponse.kt │ │ ├── TraktUserStatsResponse.kt │ │ ├── TraktVideosResponse.kt │ │ └── TraktWatchedProgressResponse.kt │ └── implementation/ │ ├── build.gradle.kts │ └── src/ │ ├── androidMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── trakt/ │ │ └── service/ │ │ └── implementation/ │ │ └── TraktPlatformBindingContainer.kt │ ├── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── trakt/ │ │ └── service/ │ │ └── implementation/ │ │ ├── TraktAuthPlugin.kt │ │ ├── TraktBindingContainer.kt │ │ ├── TraktHttpClient.kt │ │ └── api/ │ │ ├── DefaultTraktCalendarRemoteDataSource.kt │ │ ├── DefaultTraktEpisodeRemoteDataSource.kt │ │ ├── DefaultTraktListRemoteDataSource.kt │ │ ├── DefaultTraktShowsRemoteDataSource.kt │ │ ├── DefaultTraktSyncRemoteDataSource.kt │ │ ├── DefaultTraktTokenRemoteDataSource.kt │ │ └── DefaultTraktUserRemoteDataSource.kt │ ├── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── trakt/ │ │ └── service/ │ │ └── implementation/ │ │ └── TraktAuthGuardPluginTest.kt │ ├── iosMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── trakt/ │ │ └── service/ │ │ └── implementation/ │ │ └── TraktPlatformBindingContainer.kt │ └── jvmTest/ │ ├── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── trakt/ │ │ └── service/ │ │ └── implementation/ │ │ ├── TestResourceLoader.jvm.kt │ │ └── api/ │ │ └── DefaultTraktListRemoteDataSourceTest.kt │ └── resources/ │ ├── trakt_add_show_response.json │ ├── trakt_error_response.json │ └── trakt_user_response.json ├── app/ │ ├── benchmark-rules.pro │ ├── build.gradle.kts │ ├── lint-baseline.xml │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── app/ │ │ └── test/ │ │ └── runner/ │ │ └── TvManiacInstrumentationRunner.kt │ ├── debug/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── app/ │ │ │ └── debug/ │ │ │ ├── DebugNotificationIconProvider.kt │ │ │ ├── DebugNotificationInitializer.kt │ │ │ └── di/ │ │ │ └── DebugNotificationInitializerBindingContainer.kt │ │ └── res/ │ │ └── drawable/ │ │ ├── ic_app_launcher.xml │ │ ├── ic_debug_bug.xml │ │ └── ic_launcher_foreground.xml │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── generated/ │ │ │ └── baselineProfiles/ │ │ │ ├── baseline-prof.txt │ │ │ └── startup-prof.txt │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── app/ │ │ │ ├── MainActivity.kt │ │ │ ├── TvManicApplication.kt │ │ │ ├── di/ │ │ │ │ ├── ActivityGraph.kt │ │ │ │ └── ApplicationGraph.kt │ │ │ └── util/ │ │ │ ├── AppNotificationIconProvider.kt │ │ │ └── TvManiacWorkerFactory.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_app_launcher.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ └── ic_launcher_monochrome.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ └── themes.xml │ │ ├── values-night/ │ │ │ ├── colors.xml │ │ │ └── themes.xml │ │ └── xml/ │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ ├── sharedTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── app/ │ │ └── test/ │ │ ├── BaseAppFlowTest.kt │ │ ├── TestAppComponent.kt │ │ ├── TvManiacTestApplication.kt │ │ └── compose/ │ │ ├── TvManiacTestActivity.kt │ │ ├── flows/ │ │ │ ├── calendar/ │ │ │ │ └── CalendarFlowTest.kt │ │ │ ├── discover/ │ │ │ │ ├── DiscoverToSeasonDetailsFlowTest.kt │ │ │ │ └── DiscoverToShowDetailsFollowFlowTest.kt │ │ │ ├── library/ │ │ │ │ └── LibraryFlowTest.kt │ │ │ ├── search/ │ │ │ │ └── SearchFlowTest.kt │ │ │ ├── seasons/ │ │ │ │ └── SeasonFlowTest.kt │ │ │ ├── settings/ │ │ │ │ └── SettingsFlowTest.kt │ │ │ ├── sheet/ │ │ │ │ └── EpisodeSheetFlowTest.kt │ │ │ ├── showdetails/ │ │ │ │ └── ShowDetailsFeaturesFlowTest.kt │ │ │ ├── upnext/ │ │ │ │ └── UpNextFlowTests.kt │ │ │ └── userlists/ │ │ │ └── UserListFlowTests.kt │ │ ├── journey/ │ │ │ ├── AuthenticatedUserJourneyTest.kt │ │ │ └── UnauthenticatedUserJourneyTest.kt │ │ ├── robot/ │ │ │ ├── CalendarRobot.kt │ │ │ ├── DiscoverRobot.kt │ │ │ ├── EpisodeSheetRobot.kt │ │ │ ├── HomeRobot.kt │ │ │ ├── LibraryRobot.kt │ │ │ ├── ProfileRobot.kt │ │ │ ├── ProgressRobot.kt │ │ │ ├── RootRobot.kt │ │ │ ├── SearchRobot.kt │ │ │ ├── SeasonDetailsRobot.kt │ │ │ ├── SettingsRobot.kt │ │ │ └── ShowDetailsRobot.kt │ │ └── stubs/ │ │ └── Scenarios.kt │ └── test/ │ └── kotlin/ │ └── com/ │ └── thomaskioko/ │ └── tvmaniac/ │ └── app/ │ └── test/ │ └── graph/ │ ├── GraphFactories.kt │ └── NavigationRouteTest.kt ├── benchmark/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── kotlin/ │ └── com/ │ └── thomaskioko/ │ └── tvmaniac/ │ └── benchmark/ │ ├── Common.kt │ ├── baselineprofile/ │ │ └── BaselineProfileGenerator.kt │ └── benchmark/ │ └── StartupBenchmarks.kt ├── build.gradle.kts ├── cliff.toml ├── compose-stability.conf ├── core/ │ ├── appconfig/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── appconfig/ │ │ │ └── ApplicationInfo.kt │ │ └── implementation/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── androidMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── appconfig/ │ │ │ └── AndroidAppConfigBindingContainer.kt │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── appconfig/ │ │ │ ├── DefaultTmdbConfig.kt │ │ │ └── DefaultTraktConfig.kt │ │ └── iosMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── appconfig/ │ │ └── IosAppConfigBindingContainer.kt │ ├── base/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── androidMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── core/ │ │ │ └── base/ │ │ │ └── di/ │ │ │ └── BaseAndroidBindingContainer.kt │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── core/ │ │ └── base/ │ │ ├── ActivityScope.kt │ │ ├── AppInitializers.kt │ │ ├── Initializer.kt │ │ ├── Qualifiers.kt │ │ ├── di/ │ │ │ ├── BaseBindingContainer.kt │ │ │ └── InitializerMultibindings.kt │ │ ├── extensions/ │ │ │ ├── Combine.kt │ │ │ ├── DecomposeUtils.kt │ │ │ ├── Lazy.kt │ │ │ └── ParallelUtils.kt │ │ ├── interactor/ │ │ │ └── Interactor.kt │ │ └── model/ │ │ └── AppCoroutineDispatchers.kt │ ├── connectivity/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── core/ │ │ │ └── connectivity/ │ │ │ └── api/ │ │ │ └── InternetConnectionChecker.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── androidMain/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── connectivity/ │ │ │ │ └── implementation/ │ │ │ │ └── PlatformInternetConnectionChecker.android.kt │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── connectivity/ │ │ │ │ └── implementation/ │ │ │ │ └── PlatformInternetConnectionChecker.kt │ │ │ ├── iosMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── connectivity/ │ │ │ │ └── implementation/ │ │ │ │ └── PlatformInternetConnectionChecker.ios.kt │ │ │ └── jvmMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── core/ │ │ │ └── connectivity/ │ │ │ └── implementation/ │ │ │ └── PlatformInternetConnectionChecker.jvm.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── core/ │ │ └── connectivity/ │ │ └── testing/ │ │ └── FakeInternetConnectionChecker.kt │ ├── imageloading/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── imageloading/ │ │ │ └── api/ │ │ │ └── ImageQualityProvider.kt │ │ └── implementation/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── imageloading/ │ │ └── implementation/ │ │ ├── CoilImageLoaderFactory.kt │ │ ├── CoilImageLoaderInitializer.kt │ │ ├── DefaultImageQualityProvider.kt │ │ ├── di/ │ │ │ ├── CoilImageLoaderInitializerBindingContainer.kt │ │ │ └── ImageLoadingBindingContainer.kt │ │ └── interceptors/ │ │ └── TmdbInterceptor.kt │ ├── integration/ │ │ ├── infra/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── androidHostTest/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── thomaskioko/ │ │ │ │ │ └── tvmaniac/ │ │ │ │ │ └── testing/ │ │ │ │ │ └── integration/ │ │ │ │ │ ├── EndpointsCatalogTest.kt │ │ │ │ │ ├── FixtureLoaderTest.kt │ │ │ │ │ └── MockEngineHandlerTest.kt │ │ │ │ └── resources/ │ │ │ │ └── fixtures/ │ │ │ │ └── test/ │ │ │ │ └── hello.json │ │ │ ├── androidMain/ │ │ │ │ ├── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── thomaskioko/ │ │ │ │ │ └── tvmaniac/ │ │ │ │ │ └── testing/ │ │ │ │ │ └── integration/ │ │ │ │ │ ├── Endpoints.kt │ │ │ │ │ ├── MockEngineHandler.kt │ │ │ │ │ ├── SearchStubs.kt │ │ │ │ │ ├── ShowFixtures.kt │ │ │ │ │ ├── bindings/ │ │ │ │ │ │ ├── TestAuthBindingContainer.kt │ │ │ │ │ │ ├── TestConnectivityBindingContainer.kt │ │ │ │ │ │ ├── TestDateTimeBindingContainer.kt │ │ │ │ │ │ ├── TestDispatcherBindingContainer.kt │ │ │ │ │ │ ├── TestImageLoaderBindingContainer.kt │ │ │ │ │ │ ├── TestInitializerBindingContainer.kt │ │ │ │ │ │ ├── TestLoggerBindingContainer.kt │ │ │ │ │ │ ├── TestNotificationBindingContainer.kt │ │ │ │ │ │ ├── TestTmdbBindingContainer.kt │ │ │ │ │ │ ├── TestTraktAuthManagerBindingContainer.kt │ │ │ │ │ │ ├── TestTraktBindingContainer.kt │ │ │ │ │ │ └── TestWorkerBindingContainer.kt │ │ │ │ │ └── util/ │ │ │ │ │ └── FixtureLoader.kt │ │ │ │ └── resources/ │ │ │ │ └── fixtures/ │ │ │ │ ├── empty_array.json │ │ │ │ ├── tmdb/ │ │ │ │ │ ├── credits/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ ├── details/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ ├── discover/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ └── watchproviders/ │ │ │ │ │ ├── error.json │ │ │ │ │ └── success.json │ │ │ │ └── trakt/ │ │ │ │ ├── calendar/ │ │ │ │ │ ├── error.json │ │ │ │ │ └── success.json │ │ │ │ ├── episodes/ │ │ │ │ │ ├── season1/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ └── season2/ │ │ │ │ │ ├── error.json │ │ │ │ │ └── success.json │ │ │ │ ├── genres/ │ │ │ │ │ ├── error.json │ │ │ │ │ └── success.json │ │ │ │ ├── search/ │ │ │ │ │ ├── error.json │ │ │ │ │ └── success.json │ │ │ │ ├── seasons/ │ │ │ │ │ ├── error.json │ │ │ │ │ └── success.json │ │ │ │ ├── shows/ │ │ │ │ │ ├── details/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ ├── favorite/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ ├── people/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ ├── popular/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ ├── progress/ │ │ │ │ │ │ ├── refreshed/ │ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ │ └── success.json │ │ │ │ │ │ └── watched/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ ├── related/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ ├── trending/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ └── videos/ │ │ │ │ │ ├── error.json │ │ │ │ │ └── success.json │ │ │ │ ├── sync/ │ │ │ │ │ ├── error.json │ │ │ │ │ ├── history/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ └── success.json │ │ │ │ └── users/ │ │ │ │ ├── lists/ │ │ │ │ │ ├── create/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ ├── error.json │ │ │ │ │ ├── items/ │ │ │ │ │ │ ├── add/ │ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ │ └── success.json │ │ │ │ │ │ └── remove/ │ │ │ │ │ │ ├── error.json │ │ │ │ │ │ └── success.json │ │ │ │ │ └── success.json │ │ │ │ ├── me/ │ │ │ │ │ ├── error.json │ │ │ │ │ └── success.json │ │ │ │ ├── stats/ │ │ │ │ │ ├── error.json │ │ │ │ │ └── success.json │ │ │ │ └── watchlist/ │ │ │ │ ├── error.json │ │ │ │ └── success.json │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── testing/ │ │ │ │ └── di/ │ │ │ │ ├── FakeAppConfigBindingContainer.kt │ │ │ │ └── TestScope.kt │ │ │ ├── iosMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── testing/ │ │ │ │ └── di/ │ │ │ │ ├── FakeIosPlatformBindingContainer.kt │ │ │ │ ├── RunTestWithGraph.kt │ │ │ │ └── TestGraph.kt │ │ │ ├── jvmAndIosMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── testing/ │ │ │ │ └── di/ │ │ │ │ └── FakeAppBindingContainer.kt │ │ │ ├── jvmMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── testing/ │ │ │ │ └── di/ │ │ │ │ ├── RunTestWithGraph.kt │ │ │ │ ├── TestGraph.kt │ │ │ │ └── TestJvmPlatformBindingContainer.kt │ │ │ └── jvmTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── testing/ │ │ │ └── di/ │ │ │ └── TestJvmGraphTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── androidMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── testing/ │ │ └── integration/ │ │ └── ui/ │ │ ├── BaseRobot.kt │ │ └── SystemDialogUtil.kt │ ├── locale/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── locale/ │ │ │ └── api/ │ │ │ ├── Language.kt │ │ │ └── LocaleProvider.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── androidDeviceTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── locale/ │ │ │ │ └── implementation/ │ │ │ │ └── PlatformLocaleProviderAndroidTest.kt │ │ │ ├── androidMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── locale/ │ │ │ │ └── implementation/ │ │ │ │ └── PlatformLocaleProvider.android.kt │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── locale/ │ │ │ │ └── implementation/ │ │ │ │ ├── DefaultLocaleProvider.kt │ │ │ │ └── PlatformLocaleProvider.kt │ │ │ ├── commonTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── locale/ │ │ │ │ └── implementation/ │ │ │ │ └── PlatformLocaleProviderTest.kt │ │ │ ├── iosMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── locale/ │ │ │ │ └── implementation/ │ │ │ │ └── PlatformLocaleProvider.ios.kt │ │ │ ├── iosTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── locale/ │ │ │ │ └── implementation/ │ │ │ │ └── PlatformLocaleProviderIosTest.kt │ │ │ ├── jvmMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── locale/ │ │ │ │ └── implementation/ │ │ │ │ └── PlatformLocaleProvider.jvm.kt │ │ │ └── jvmTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── locale/ │ │ │ └── implementation/ │ │ │ └── PlatformLocaleProviderJvmTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── locale/ │ │ └── testing/ │ │ └── FakeLocaleProvider.kt │ ├── logger/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── logger/ │ │ │ │ ├── CrashReporter.kt │ │ │ │ └── Logger.kt │ │ │ └── iosMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── core/ │ │ │ └── logger/ │ │ │ ├── CrashReportingBridge.kt │ │ │ └── CrashReportingBridgeHolder.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── androidMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── logger/ │ │ │ │ ├── AndroidCrashReporter.kt │ │ │ │ └── AndroidLoggerBindingContainer.kt │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── logger/ │ │ │ │ ├── CompositeLogger.kt │ │ │ │ ├── FirebaseCrashLogger.kt │ │ │ │ ├── KermitLogger.kt │ │ │ │ └── LoggingInitializer.kt │ │ │ └── iosMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── core/ │ │ │ └── logger/ │ │ │ ├── IosCrashReporter.kt │ │ │ ├── IosCrashReporterBindingContainer.kt │ │ │ └── NoOpCrashReportingBridge.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── core/ │ │ └── logger/ │ │ └── fixture/ │ │ ├── FakeCrashReporter.kt │ │ └── FakeLogger.kt │ ├── network-util/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── networkutil/ │ │ │ │ └── api/ │ │ │ │ ├── ApiRateLimiter.kt │ │ │ │ ├── extensions/ │ │ │ │ │ ├── ApiRateLimiterExtensions.kt │ │ │ │ │ ├── ApiResponseExtensions.kt │ │ │ │ │ ├── InternetConnectionPlugin.kt │ │ │ │ │ └── StoreExtensions.kt │ │ │ │ └── model/ │ │ │ │ ├── ApiExceptions.kt │ │ │ │ ├── ApiResponse.kt │ │ │ │ ├── AuthenticationException.kt │ │ │ │ ├── HttpExceptions.kt │ │ │ │ ├── NoInternetException.kt │ │ │ │ ├── SyncError.kt │ │ │ │ └── SyncException.kt │ │ │ ├── commonTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── networkutil/ │ │ │ │ └── api/ │ │ │ │ └── model/ │ │ │ │ └── ThrowableToSyncErrorTest.kt │ │ │ └── jvmTest/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── networkutil/ │ │ │ │ └── api/ │ │ │ │ └── extensions/ │ │ │ │ ├── ApiResponseExtensionsTest.kt │ │ │ │ ├── InternetConnectionPluginTest.kt │ │ │ │ └── TestResourceLoader.jvm.kt │ │ │ └── resources/ │ │ │ ├── error_response.json │ │ │ └── success_response.json │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── networkutil/ │ │ │ │ └── ratelimit/ │ │ │ │ └── AdaptiveApiRateLimiter.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── core/ │ │ │ └── networkutil/ │ │ │ ├── model/ │ │ │ │ └── SyncErrorTest.kt │ │ │ └── ratelimit/ │ │ │ └── AdaptiveApiRateLimiterTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── core/ │ │ └── networkutil/ │ │ └── testing/ │ │ └── FakeApiRateLimiter.kt │ ├── notifications/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── androidMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── notifications/ │ │ │ │ └── api/ │ │ │ │ └── NotificationIconProvider.kt │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── core/ │ │ │ └── notifications/ │ │ │ └── api/ │ │ │ ├── EpisodeNotification.kt │ │ │ ├── NotificationChannel.kt │ │ │ └── NotificationManager.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── androidMain/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── notifications/ │ │ │ │ └── implementation/ │ │ │ │ ├── AndroidNotificationManager.kt │ │ │ │ ├── BootCompletedReceiver.kt │ │ │ │ ├── DebugNotificationManager.kt │ │ │ │ ├── EpisodeNotificationReceiver.kt │ │ │ │ ├── PendingNotificationsStore.kt │ │ │ │ └── model/ │ │ │ │ └── StoredNotification.kt │ │ │ └── iosMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── core/ │ │ │ └── notifications/ │ │ │ └── implementation/ │ │ │ └── IosNotificationManager.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── core/ │ │ └── notifications/ │ │ └── testing/ │ │ └── FakeNotificationManager.kt │ ├── paging/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── core/ │ │ └── paging/ │ │ ├── CommonPagingConfig.kt │ │ ├── KeyedQueryPagingSource.kt │ │ ├── OffsetQueryPagingSource.kt │ │ ├── PaginatedRemoteMediator.kt │ │ └── QueryPagingSource.kt │ ├── screenshot-tests/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── screenshottests/ │ │ └── RoborazziScreenshotUtil.kt │ ├── tasks/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── core/ │ │ │ └── tasks/ │ │ │ └── api/ │ │ │ ├── BackgroundTaskScheduler.kt │ │ │ ├── BackgroundWorker.kt │ │ │ ├── PeriodicTaskRequest.kt │ │ │ ├── TaskConstraints.kt │ │ │ ├── WorkerFactory.kt │ │ │ └── WorkerResult.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── androidMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── tasks/ │ │ │ │ └── implementation/ │ │ │ │ ├── AndroidTaskScheduler.kt │ │ │ │ ├── SchedulerDispatchWorker.kt │ │ │ │ └── di/ │ │ │ │ └── WorkManagerBindingContainer.kt │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── core/ │ │ │ │ └── tasks/ │ │ │ │ └── implementation/ │ │ │ │ └── DefaultWorkerFactory.kt │ │ │ └── iosMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── core/ │ │ │ └── tasks/ │ │ │ └── implementation/ │ │ │ └── IosTaskScheduler.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── core/ │ │ └── tasks/ │ │ └── testing/ │ │ └── FakeBackgroundTaskScheduler.kt │ ├── test-tags/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── testtags/ │ │ ├── calendar/ │ │ │ └── CalendarTestTags.kt │ │ ├── component/ │ │ │ └── DesignComponentTestTags.kt │ │ ├── discover/ │ │ │ └── DiscoverTestTags.kt │ │ ├── episodesheet/ │ │ │ └── EpisodeSheetTestTags.kt │ │ ├── home/ │ │ │ └── HomeTestTags.kt │ │ ├── library/ │ │ │ └── LibraryTestTags.kt │ │ ├── moreshows/ │ │ │ └── MoreShowsTestTags.kt │ │ ├── notifications/ │ │ │ └── NotificationRationaleTestTags.kt │ │ ├── profile/ │ │ │ └── ProfileTestTags.kt │ │ ├── progress/ │ │ │ └── ProgressTestTags.kt │ │ ├── search/ │ │ │ └── SearchTestTags.kt │ │ ├── seasondetails/ │ │ │ └── SeasonDetailsTestTags.kt │ │ ├── settings/ │ │ │ └── SettingsTestTags.kt │ │ ├── showdetails/ │ │ │ └── ShowDetailsTestTags.kt │ │ └── upnext/ │ │ └── UpNextTestTags.kt │ ├── util/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── util/ │ │ │ └── api/ │ │ │ ├── AppUtils.kt │ │ │ ├── DateTimeProvider.kt │ │ │ ├── FormatterUtil.kt │ │ │ └── ItemSyncer.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── androidMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── util/ │ │ │ │ ├── AndroidAppUtils.kt │ │ │ │ └── AndroidFormatterUtil.kt │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── util/ │ │ │ │ ├── DateTimeBindingContainer.kt │ │ │ │ └── DefaultDateTimeProvider.kt │ │ │ ├── commonTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── util/ │ │ │ │ └── DefaultDateTimeProviderTest.kt │ │ │ ├── iosMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── util/ │ │ │ │ ├── IosAppUtils.kt │ │ │ │ └── IosFormatterUtil.kt │ │ │ ├── iosTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── util/ │ │ │ │ └── IosFormatterUtilTest.kt │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── util/ │ │ │ └── AndroidFormatterUtilTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── util/ │ │ │ └── testing/ │ │ │ ├── FakeApplicationInfo.kt │ │ │ ├── FakeDateTimeProvider.kt │ │ │ ├── FakeFormatterUtil.kt │ │ │ └── FlakyTests.kt │ │ └── jvmAndroidMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── util/ │ │ └── testing/ │ │ └── FlakyTestRule.kt │ └── view/ │ ├── build.gradle.kts │ └── src/ │ └── commonMain/ │ └── kotlin/ │ └── com/ │ └── thomaskioko/ │ └── tvmaniac/ │ └── core/ │ └── view/ │ ├── ErrorToStringMapper.kt │ ├── InvokeStatus.kt │ ├── ObservableLoadingCounter.kt │ └── UiMessage.kt ├── data/ │ ├── calendar/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── calendar/ │ │ │ ├── CalendarDao.kt │ │ │ ├── CalendarEntry.kt │ │ │ └── CalendarRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── calendar/ │ │ │ └── implementation/ │ │ │ ├── CalendarStore.kt │ │ │ ├── DefaultCalendarDao.kt │ │ │ └── DefaultCalendarRepository.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── calendar/ │ │ └── testing/ │ │ └── FakeCalendarRepository.kt │ ├── cast/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── cast/ │ │ │ └── api/ │ │ │ ├── CastDao.kt │ │ │ └── CastRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── cast/ │ │ │ └── implementation/ │ │ │ ├── DefaultCastDao.kt │ │ │ ├── DefaultCastRepository.kt │ │ │ ├── ShowCastResult.kt │ │ │ └── ShowCastStore.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── cast/ │ │ └── testing/ │ │ └── FakeCastRepository.kt │ ├── database/ │ │ ├── sqldelight/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── androidMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── db/ │ │ │ │ └── DatabasePlatformBindingContainer.kt │ │ │ ├── commonMain/ │ │ │ │ └── sqldelight/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ ├── db/ │ │ │ │ │ ├── Calendar.sq │ │ │ │ │ ├── Cast.sq │ │ │ │ │ ├── EpisodeImage.sq │ │ │ │ │ ├── Episodes.sq │ │ │ │ │ ├── FeaturedShows.sq │ │ │ │ │ ├── FollowedShows.sq │ │ │ │ │ ├── GenreShows.sq │ │ │ │ │ ├── Genres.sq │ │ │ │ │ ├── LastRequests.sq │ │ │ │ │ ├── Library.sq │ │ │ │ │ ├── NextEpisodes.sq │ │ │ │ │ ├── PopularShows.sq │ │ │ │ │ ├── RecommendedShows.sq │ │ │ │ │ ├── SeasonImages.sq │ │ │ │ │ ├── SeasonVideos.sq │ │ │ │ │ ├── Seasons.sq │ │ │ │ │ ├── ShowGenres.sq │ │ │ │ │ ├── ShowMetadata.sq │ │ │ │ │ ├── ShowsLastWatched.sq │ │ │ │ │ ├── ShowsNextToWatch.sq │ │ │ │ │ ├── SimilarShows.sq │ │ │ │ │ ├── Stats.sq │ │ │ │ │ ├── TopratedShows.sq │ │ │ │ │ ├── Trailers.sq │ │ │ │ │ ├── TraktGenres.sq │ │ │ │ │ ├── TraktLastActivity.sq │ │ │ │ │ ├── TraktListShows.sq │ │ │ │ │ ├── TraktLists.sq │ │ │ │ │ ├── TrendingShows.sq │ │ │ │ │ ├── TvShow.sq │ │ │ │ │ ├── UpcomingShows.sq │ │ │ │ │ ├── User.sq │ │ │ │ │ ├── WatchProviders.sq │ │ │ │ │ └── WatchedEpisodes.sq │ │ │ │ └── migrations/ │ │ │ │ ├── 1.sqm │ │ │ │ ├── 10.sqm │ │ │ │ ├── 11.sqm │ │ │ │ ├── 12.sqm │ │ │ │ ├── 13.sqm │ │ │ │ ├── 14.sqm │ │ │ │ ├── 15.sqm │ │ │ │ ├── 16.sqm │ │ │ │ ├── 17.sqm │ │ │ │ ├── 18.sqm │ │ │ │ ├── 19.sqm │ │ │ │ ├── 2.sqm │ │ │ │ ├── 20.sqm │ │ │ │ ├── 21.sqm │ │ │ │ ├── 22.sqm │ │ │ │ ├── 23.sqm │ │ │ │ ├── 3.sqm │ │ │ │ ├── 4.sqm │ │ │ │ ├── 5.sqm │ │ │ │ ├── 6.sqm │ │ │ │ ├── 7.sqm │ │ │ │ ├── 8.sqm │ │ │ │ └── 9.sqm │ │ │ ├── iosMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── db/ │ │ │ │ └── DatabasePlatformBindingContainer.kt │ │ │ └── jvmTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── db/ │ │ │ ├── Migration22TraktListShowsTest.kt │ │ │ ├── Migration23DropParentFkTest.kt │ │ │ ├── SchemaCreateTest.kt │ │ │ └── util/ │ │ │ └── MigrationTestUtil.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── androidMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── database/ │ │ │ └── test/ │ │ │ └── BaseDatabaseTest.android.kt │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── database/ │ │ │ └── test/ │ │ │ └── BaseDatabaseTest.kt │ │ ├── iosMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── database/ │ │ │ └── test/ │ │ │ └── BaseDatabaseTest.ios.kt │ │ └── jvmMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── database/ │ │ └── test/ │ │ └── BaseDatabaseTest.jvm.kt │ ├── datastore/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── datastore/ │ │ │ └── api/ │ │ │ ├── AppTheme.kt │ │ │ ├── DatastoreRepository.kt │ │ │ ├── ImageQuality.kt │ │ │ └── ListStyle.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ ├── src/ │ │ │ │ ├── androidMain/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── thomaskioko/ │ │ │ │ │ └── tvmaniac/ │ │ │ │ │ └── datastore/ │ │ │ │ │ └── implementation/ │ │ │ │ │ └── DataStorePlatformBindingContainer.kt │ │ │ │ ├── commonMain/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── thomaskioko/ │ │ │ │ │ └── tvmaniac/ │ │ │ │ │ └── datastore/ │ │ │ │ │ └── implementation/ │ │ │ │ │ ├── DataStoreHelper.kt │ │ │ │ │ └── DefaultDatastoreRepository.kt │ │ │ │ ├── commonTest/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── thomaskioko/ │ │ │ │ │ └── tvmaniac/ │ │ │ │ │ └── datastore/ │ │ │ │ │ └── implemetation/ │ │ │ │ │ └── DatastoreRepositoryTest.kt │ │ │ │ └── iosMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── datastore/ │ │ │ │ └── implementation/ │ │ │ │ └── DataStorePlatformBindingContainer.kt │ │ │ └── test.preferences_pb │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── datastore/ │ │ └── testing/ │ │ └── FakeDatastoreRepository.kt │ ├── episode/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── episodes/ │ │ │ └── api/ │ │ │ ├── EpisodeRepository.kt │ │ │ ├── EpisodeWatchesDataSource.kt │ │ │ ├── EpisodesDao.kt │ │ │ ├── NextEpisodeDao.kt │ │ │ ├── WatchedEpisodeDao.kt │ │ │ ├── WatchedEpisodeEntry.kt │ │ │ ├── WatchedEpisodeSyncRepository.kt │ │ │ └── model/ │ │ │ ├── EpisodeExtensions.kt │ │ │ ├── EpisodeWatchParams.kt │ │ │ ├── LastWatchedEpisode.kt │ │ │ ├── SeasonWatchProgress.kt │ │ │ ├── ShowWatchProgress.kt │ │ │ ├── UnwatchedEpisode.kt │ │ │ ├── UpcomingEpisode.kt │ │ │ ├── WatchProgress.kt │ │ │ └── WatchedEpisode.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── episodes/ │ │ │ │ └── implementation/ │ │ │ │ ├── DefaultEpisodeRepository.kt │ │ │ │ ├── DefaultWatchedEpisodeSyncRepository.kt │ │ │ │ ├── EpisodeWatchesLastRequestStore.kt │ │ │ │ ├── TraktEpisodeWatchesDataSource.kt │ │ │ │ ├── UpcomingEpisodesStore.kt │ │ │ │ ├── dao/ │ │ │ │ │ ├── DefaultEpisodesDao.kt │ │ │ │ │ ├── DefaultNextEpisodeDao.kt │ │ │ │ │ └── DefaultWatchedEpisodeDao.kt │ │ │ │ └── model/ │ │ │ │ └── NextEpisodeKey.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── episodes/ │ │ │ └── implementation/ │ │ │ ├── DefaultEpisodeRepositoryTest.kt │ │ │ ├── DefaultEpisodesDaoTest.kt │ │ │ ├── DefaultNextEpisodeDaoTest.kt │ │ │ ├── DefaultWatchedEpisodeDaoTest.kt │ │ │ ├── EpisodesCacheTest.kt │ │ │ └── MockData.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── episodes/ │ │ └── testing/ │ │ ├── FakeEpisodeRepository.kt │ │ ├── FakeEpisodeWatchesDataSource.kt │ │ └── FakeWatchedEpisodeSyncRepository.kt │ ├── featuredshows/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── featuredshows/ │ │ │ └── api/ │ │ │ ├── FeaturedShowsDao.kt │ │ │ ├── FeaturedShowsRepository.kt │ │ │ └── interactor/ │ │ │ └── FeaturedShowsInteractor.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── data/ │ │ │ │ └── featuredshows/ │ │ │ │ └── implementation/ │ │ │ │ ├── DefaultFeaturedShowsDao.kt │ │ │ │ ├── DefaultFeaturedShowsRepository.kt │ │ │ │ └── FeaturedShowsStore.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── featuredshows/ │ │ │ └── implementation/ │ │ │ └── DefaultFeaturedShowsDaoTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── featuredshows/ │ │ └── testing/ │ │ └── FakeFeaturedShowsRepository.kt │ ├── followedshows/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── followedshows/ │ │ │ └── api/ │ │ │ ├── FollowedShowEntry.kt │ │ │ ├── FollowedShowsDao.kt │ │ │ ├── FollowedShowsRepository.kt │ │ │ └── PendingAction.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── followedshows/ │ │ │ │ └── implementation/ │ │ │ │ ├── DefaultFollowedShowsDao.kt │ │ │ │ └── DefaultFollowedShowsRepository.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── followedshows/ │ │ │ └── implementation/ │ │ │ ├── DefaultFollowedShowsDaoTest.kt │ │ │ └── DefaultFollowedShowsRepositoryTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── followedshows/ │ │ └── testing/ │ │ ├── FakeFollowedShowsDao.kt │ │ └── FakeFollowedShowsRepository.kt │ ├── genre/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── genre/ │ │ │ ├── GenreDao.kt │ │ │ ├── GenreRepository.kt │ │ │ ├── ShowGenresEntity.kt │ │ │ ├── TraktGenreDao.kt │ │ │ └── model/ │ │ │ ├── GenreShowCategory.kt │ │ │ ├── GenreShowsStoreKey.kt │ │ │ ├── GenreWithShowsEntity.kt │ │ │ └── TraktGenreEntity.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── genre/ │ │ │ ├── DefaultGenreDao.kt │ │ │ ├── DefaultGenreRepository.kt │ │ │ ├── DefaultTraktGenreDao.kt │ │ │ ├── GenrePosterStore.kt │ │ │ ├── GenreShowsStore.kt │ │ │ ├── GenreStore.kt │ │ │ ├── ShowsByGenreIdStore.kt │ │ │ └── TraktGenresStore.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── genre/ │ │ └── FakeGenreRepository.kt │ ├── library/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── library/ │ │ │ ├── LibraryDao.kt │ │ │ ├── LibraryRepository.kt │ │ │ └── model/ │ │ │ ├── LibraryItem.kt │ │ │ ├── LibrarySortOption.kt │ │ │ └── WatchProvider.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── library/ │ │ │ └── implementation/ │ │ │ ├── DefaultLibraryDao.kt │ │ │ ├── DefaultLibraryRepository.kt │ │ │ └── LibraryStore.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── library/ │ │ └── testing/ │ │ └── FakeLibraryRepository.kt │ ├── popularshows/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── popularshows/ │ │ │ └── api/ │ │ │ ├── PopularShowsDao.kt │ │ │ ├── PopularShowsInteractor.kt │ │ │ └── PopularShowsRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── data/ │ │ │ │ └── popularshows/ │ │ │ │ └── implementation/ │ │ │ │ ├── DefaultPopularShowsDao.kt │ │ │ │ ├── DefaultPopularShowsRepository.kt │ │ │ │ └── PopularShowsStore.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── popularshows/ │ │ │ └── implementation/ │ │ │ └── DefaultPopularShowsDaoTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── popularshows/ │ │ └── testing/ │ │ └── FakePopularShowsRepository.kt │ ├── recommendedshows/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── recommendedshows/ │ │ │ └── api/ │ │ │ ├── RecommendedShowsDao.kt │ │ │ ├── RecommendedShowsParams.kt │ │ │ └── RecommendedShowsRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── recommendedshows/ │ │ │ └── implementation/ │ │ │ ├── DefaultRecommendedShowsDao.kt │ │ │ ├── DefaultRecommendedShowsRepository.kt │ │ │ ├── RecommendedShowResult.kt │ │ │ └── RecommendedShowsStore.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── recommendedshows/ │ │ └── testing/ │ │ └── FakeRecommendedShowsRepository.kt │ ├── request-manager/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── resourcemanager/ │ │ │ └── api/ │ │ │ ├── RequestManagerRepository.kt │ │ │ └── RequestTypeConfig.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── resourcemanager/ │ │ │ │ └── implementation/ │ │ │ │ └── DefaultRequestManagerRepository.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── resourcemanager/ │ │ │ └── implementation/ │ │ │ ├── CacheValidationTest.kt │ │ │ └── DefaultRequestManagerRepositoryTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── requestmanager/ │ │ └── testing/ │ │ └── FakeRequestManagerRepository.kt │ ├── search/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── search/ │ │ │ └── api/ │ │ │ └── SearchRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── search/ │ │ │ └── implementation/ │ │ │ ├── DefaultSearchRepository.kt │ │ │ ├── SearchShowResult.kt │ │ │ └── SearchShowStore.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── search/ │ │ └── testing/ │ │ └── FakeSearchRepository.kt │ ├── seasondetails/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── seasondetails/ │ │ │ └── api/ │ │ │ ├── SeasonDetailsDao.kt │ │ │ ├── SeasonDetailsParam.kt │ │ │ ├── SeasonDetailsRepository.kt │ │ │ └── model/ │ │ │ ├── ContinueTrackingResult.kt │ │ │ ├── EpisodeDetails.kt │ │ │ └── SeasonDetailsWithEpisodes.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── seasondetails/ │ │ │ └── implementation/ │ │ │ ├── DefaultSeasonDetailsDao.kt │ │ │ ├── DefaultSeasonDetailsRepository.kt │ │ │ ├── SeasonDetailsResponse.kt │ │ │ └── SeasonDetailsStore.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── seasondetails/ │ │ └── testing/ │ │ └── FakeSeasonDetailsRepository.kt │ ├── seasons/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── seasons/ │ │ │ └── api/ │ │ │ ├── FollowedShowSeason.kt │ │ │ ├── SeasonsDao.kt │ │ │ ├── SeasonsEpisodesSyncRepository.kt │ │ │ └── SeasonsRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── seasons/ │ │ │ └── implementation/ │ │ │ ├── DefaultSeasonsDao.kt │ │ │ ├── DefaultSeasonsEpisodesSyncRepository.kt │ │ │ ├── DefaultSeasonsRepository.kt │ │ │ └── SeasonsWithEpisodesStore.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── seasons/ │ │ └── testing/ │ │ └── FakeSeasonsRepository.kt │ ├── showdetails/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── showdetails/ │ │ │ └── api/ │ │ │ ├── ShowDetailsDao.kt │ │ │ └── ShowDetailsRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── showdetails/ │ │ │ └── implementation/ │ │ │ ├── DefaultShowDetailsDao.kt │ │ │ ├── DefaultShowDetailsRepository.kt │ │ │ ├── ShowDetailsResponse.kt │ │ │ └── ShowDetailsStore.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── showdetails/ │ │ └── testing/ │ │ └── FakeShowDetailsRepository.kt │ ├── shows/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── shows/ │ │ │ │ └── api/ │ │ │ │ ├── MergeShowUtil.kt │ │ │ │ ├── TvShowsDao.kt │ │ │ │ └── model/ │ │ │ │ ├── Category.kt │ │ │ │ ├── ShowDefaults.kt │ │ │ │ └── ShowEntity.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── shows/ │ │ │ └── api/ │ │ │ ├── MockData.kt │ │ │ └── TvShowCacheTest.kt │ │ └── implementation/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── shows/ │ │ └── implementation/ │ │ └── DefaultTvShowsDao.kt │ ├── similar/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── similar/ │ │ │ └── api/ │ │ │ ├── SimilarShowsDao.kt │ │ │ └── SimilarShowsRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── similar/ │ │ │ └── implementation/ │ │ │ ├── DefaultSimilarShowsDao.kt │ │ │ ├── DefaultSimilarShowsRepository.kt │ │ │ ├── SimilarParams.kt │ │ │ ├── SimilarShowResult.kt │ │ │ └── SimilarShowStore.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── similar/ │ │ └── testing/ │ │ └── FakeSimilarShowsRepository.kt │ ├── sync-activity/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── syncactivity/ │ │ │ └── api/ │ │ │ ├── TraktActivityDao.kt │ │ │ ├── TraktActivityRepository.kt │ │ │ └── model/ │ │ │ ├── ActivityType.kt │ │ │ └── TraktLastActivity.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── syncactivity/ │ │ │ │ └── implementation/ │ │ │ │ ├── DefaultTraktActivityDao.kt │ │ │ │ ├── DefaultTraktActivityRepository.kt │ │ │ │ └── TraktActivityStore.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── syncactivity/ │ │ │ └── implementation/ │ │ │ └── DefaultTraktActivityDaoTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── syncactivity/ │ │ └── testing/ │ │ ├── FakeTraktActivityDao.kt │ │ └── FakeTraktActivityRepository.kt │ ├── topratedshows/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── topratedshows/ │ │ │ └── data/ │ │ │ └── api/ │ │ │ ├── TopRatedShowsDao.kt │ │ │ ├── TopRatedShowsInteractor.kt │ │ │ └── TopRatedShowsRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── toprated/ │ │ │ │ └── data/ │ │ │ │ └── implementation/ │ │ │ │ ├── DefaultTopRatedShowsDao.kt │ │ │ │ ├── DefaultTopRatedShowsRepository.kt │ │ │ │ ├── TopRatedShowWithImages.kt │ │ │ │ └── TopRatedShowsStore.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── toprated/ │ │ │ └── data/ │ │ │ └── implementation/ │ │ │ └── DefaultTopRatedShowsDaoTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── topratedshows/ │ │ └── testing/ │ │ └── FakeTopRatedShowsRepository.kt │ ├── trailers/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com.thomaskioko.tvmaniac.data.trailers.implementation/ │ │ │ ├── TrailerDao.kt │ │ │ └── TrailerRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── trailers/ │ │ │ └── implementation/ │ │ │ ├── DefaultTrailerDao.kt │ │ │ ├── DefaultTrailerRepository.kt │ │ │ └── TrailerStore.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── trailers/ │ │ └── testing/ │ │ ├── FakeTrailerRepository.kt │ │ └── MockData.kt │ ├── traktauth/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── traktauth/ │ │ │ └── api/ │ │ │ ├── AuthError.kt │ │ │ ├── AuthState.kt │ │ │ ├── AuthStore.kt │ │ │ ├── RefreshTokenResult.kt │ │ │ ├── TokenRefreshResult.kt │ │ │ ├── TraktAuthManager.kt │ │ │ ├── TraktAuthRepository.kt │ │ │ ├── TraktAuthState.kt │ │ │ └── TraktRefreshTokenAction.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── androidMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── traktauth/ │ │ │ │ └── implementation/ │ │ │ │ ├── AndroidAuthStore.kt │ │ │ │ ├── AndroidTraktAuthManager.kt │ │ │ │ ├── TraktActivityResultContract.kt │ │ │ │ └── di/ │ │ │ │ └── TraktAuthAndroidBindingContainer.kt │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── traktauth/ │ │ │ │ └── implementation/ │ │ │ │ ├── DefaultTraktAuthRepository.kt │ │ │ │ ├── DefaultTraktRefreshTokenAction.kt │ │ │ │ ├── TokenRefreshInitializer.kt │ │ │ │ ├── TokenRefreshWorker.kt │ │ │ │ └── di/ │ │ │ │ └── TokenRefreshInitializerBindingContainer.kt │ │ │ └── iosMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── traktauth/ │ │ │ └── implementation/ │ │ │ ├── DefaultIOSTraktAuthManager.kt │ │ │ └── IosAuthStore.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── traktauth/ │ │ └── testing/ │ │ ├── FakeAuthStore.kt │ │ ├── FakeTraktAuthManager.kt │ │ ├── FakeTraktAuthRepository.kt │ │ └── FakeTraktRefreshTokenAction.kt │ ├── traktlists/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── traktlists/ │ │ │ └── api/ │ │ │ ├── TraktList.kt │ │ │ ├── TraktListDao.kt │ │ │ ├── TraktListEntity.kt │ │ │ ├── TraktListRepository.kt │ │ │ ├── TraktListShowDao.kt │ │ │ └── TraktListShowEntry.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── traktlists/ │ │ │ └── implementation/ │ │ │ ├── CreateTraktListStore.kt │ │ │ ├── DefaultTraktListDao.kt │ │ │ ├── DefaultTraktListRepository.kt │ │ │ ├── DefaultTraktListShowDao.kt │ │ │ └── TraktListsStore.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── traktlists/ │ │ └── testing/ │ │ └── FakeTraktListRepository.kt │ ├── trendingshows/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── discover/ │ │ │ └── api/ │ │ │ ├── TrendingShowsDao.kt │ │ │ ├── TrendingShowsInteractor.kt │ │ │ ├── TrendingShowsParams.kt │ │ │ └── TrendingShowsRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── discover/ │ │ │ │ └── implementation/ │ │ │ │ ├── DefaultTrendingShowsDao.kt │ │ │ │ ├── DefaultTrendingShowsRepository.kt │ │ │ │ ├── TrendingShowWithImages.kt │ │ │ │ └── TrendingShowsStore.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── discover/ │ │ │ └── implementation/ │ │ │ └── DefaultTrendingShowsDaoTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── trendingshows/ │ │ └── testing/ │ │ └── FakeTrendingShowsRepository.kt │ ├── upcomingshows/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── upcomingshows/ │ │ │ └── api/ │ │ │ ├── UpcomingShowsDao.kt │ │ │ ├── UpcomingShowsInteractor.kt │ │ │ └── UpcomingShowsRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── data/ │ │ │ │ └── upcomingshows/ │ │ │ │ └── implementation/ │ │ │ │ ├── DefaultUpcomingShowsDao.kt │ │ │ │ ├── DefaultUpcomingShowsRepository.kt │ │ │ │ ├── UpcomingShowsStore.kt │ │ │ │ └── model/ │ │ │ │ ├── UpcomingParams.kt │ │ │ │ └── UpcomingShowResult.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── upcomingshows/ │ │ │ └── implementation/ │ │ │ └── DefaultUpcomingShowsDaoTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── upcomingshows/ │ │ └── testing/ │ │ └── FakeUpcomingShowsRepository.kt │ ├── upnext/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── upnext/ │ │ │ └── api/ │ │ │ ├── UpNextDao.kt │ │ │ ├── UpNextRepository.kt │ │ │ └── model/ │ │ │ └── NextEpisodeWithShow.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── upnext/ │ │ │ │ └── implementation/ │ │ │ │ ├── DefaultUpNextDao.kt │ │ │ │ ├── DefaultUpNextRepository.kt │ │ │ │ └── ShowUpNextStore.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── upnext/ │ │ │ └── implementation/ │ │ │ ├── DefaultUpNextDaoTest.kt │ │ │ └── DefaultUpNextRepositoryTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── upnext/ │ │ └── testing/ │ │ ├── FakeUpNextDao.kt │ │ └── FakeUpNextRepository.kt │ ├── user/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── user/ │ │ │ └── api/ │ │ │ ├── UserDao.kt │ │ │ ├── UserRepository.kt │ │ │ ├── UserStatsDao.kt │ │ │ └── model/ │ │ │ ├── UserProfile.kt │ │ │ ├── UserProfileStats.kt │ │ │ └── UserWatchTime.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── data/ │ │ │ │ └── user/ │ │ │ │ └── implementation/ │ │ │ │ ├── DefaultUserDao.kt │ │ │ │ ├── DefaultUserRepository.kt │ │ │ │ ├── DefaultUserStatsDao.kt │ │ │ │ ├── UserStatsStore.kt │ │ │ │ └── UserStore.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── data/ │ │ │ └── user/ │ │ │ └── implementation/ │ │ │ ├── DefaultUserDaoTest.kt │ │ │ └── DefaultUserStatsDaoTest.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── user/ │ │ └── testing/ │ │ └── FakeUserRepository.kt │ ├── watchlist/ │ │ ├── api/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── shows/ │ │ │ └── api/ │ │ │ ├── WatchlistDao.kt │ │ │ └── WatchlistRepository.kt │ │ ├── implementation/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── watchlist/ │ │ │ └── implementation/ │ │ │ ├── DefaultWatchlistDao.kt │ │ │ └── DefaultWatchlistRepository.kt │ │ └── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── watchlist/ │ │ └── testing/ │ │ └── FakeWatchlistRepository.kt │ └── watchproviders/ │ ├── api/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── watchproviders/ │ │ └── api/ │ │ ├── WatchProviderDao.kt │ │ └── WatchProviderRepository.kt │ ├── implementation/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── data/ │ │ └── watchproviders/ │ │ └── implementation/ │ │ ├── DefaultWatchProviderDao.kt │ │ ├── DefaultWatchProviderRepository.kt │ │ └── WatchProvidersStore.kt │ └── testing/ │ ├── build.gradle.kts │ └── src/ │ └── commonMain/ │ └── kotlin/ │ └── com/ │ └── thomaskioko/ │ └── tvmaniac/ │ └── data/ │ └── watchproviders/ │ └── testing/ │ └── FakeWatchProviderRepository.kt ├── docs/ │ ├── architecture/ │ │ ├── README.md │ │ ├── data-layer.md │ │ ├── dependency-injection.md │ │ ├── integration-testing.md │ │ ├── journey-tests.md │ │ ├── modularization.md │ │ ├── navigation-codegen.md │ │ ├── navigation.md │ │ ├── presentation-layer.md │ │ └── scopes.md │ ├── privacy_policy.md │ ├── setup.md │ └── terms_conditions.md ├── domain/ │ ├── calendar/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── domain/ │ │ │ └── calendar/ │ │ │ ├── CalendarEpisodeFormatter.kt │ │ │ ├── CalendarWeekCalculator.kt │ │ │ ├── FetchCalendarInteractor.kt │ │ │ ├── ObserveCalendarInteractor.kt │ │ │ └── model/ │ │ │ ├── DateLabel.kt │ │ │ ├── GroupedCalendarEntry.kt │ │ │ └── GroupedEpisodeEntry.kt │ │ └── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── calendar/ │ │ ├── CalendarWeekCalculatorTest.kt │ │ └── ObserveCalendarInteractorTest.kt │ ├── discover/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── domain/ │ │ │ └── discover/ │ │ │ └── DiscoverShowsInteractor.kt │ │ └── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── discover/ │ │ └── DiscoverShowsInteractorTest.kt │ ├── episode/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── domain/ │ │ │ └── episode/ │ │ │ ├── MarkEpisodeUnwatchedInteractor.kt │ │ │ ├── MarkEpisodeWatchedInteractor.kt │ │ │ ├── ObserveEpisodeByIdInteractor.kt │ │ │ └── ObserveShowWatchProgressInteractor.kt │ │ └── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── episode/ │ │ └── MarkEpisodeWatchedInteractorTest.kt │ ├── followedshows/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── followedshows/ │ │ └── UnfollowShowInteractor.kt │ ├── genre/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── genre/ │ │ ├── FetchGenreContentInteractor.kt │ │ └── GenreShowsInteractor.kt │ ├── library/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── library/ │ │ ├── LibrarySyncWorker.kt │ │ ├── ObserveLibraryInteractor.kt │ │ ├── SyncLibraryInteractor.kt │ │ ├── SyncTasksInitializer.kt │ │ └── di/ │ │ └── SyncTasksInitializerBindingContainer.kt │ ├── logout/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── logout/ │ │ └── LogoutInteractor.kt │ ├── notifications/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── domain/ │ │ │ └── notifications/ │ │ │ ├── EpisodeNotificationWorker.kt │ │ │ ├── NotificationTasksInitializer.kt │ │ │ ├── di/ │ │ │ │ └── NotificationTasksInitializerBindingContainer.kt │ │ │ └── interactor/ │ │ │ ├── RefreshUpcomingSeasonDetailsInteractor.kt │ │ │ ├── ScheduleDebugEpisodeNotificationInteractor.kt │ │ │ ├── ScheduleEpisodeNotificationsInteractor.kt │ │ │ ├── SyncTraktCalendarInteractor.kt │ │ │ └── ToggleEpisodeNotificationsInteractor.kt │ │ └── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── notifications/ │ │ └── interactor/ │ │ └── ScheduleEpisodeNotificationsInteractorTest.kt │ ├── recommendedshows/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── recommendedshows/ │ │ └── RecommendedShowsInteractor.kt │ ├── seasondetails/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── seasondetails/ │ │ ├── FetchPreviousSeasonsInteractor.kt │ │ ├── MarkSeasonUnwatchedInteractor.kt │ │ ├── MarkSeasonWatchedInteractor.kt │ │ ├── ObservableSeasonDetailsInteractor.kt │ │ ├── ObserveSeasonWatchProgressInteractor.kt │ │ ├── ObserveUnwatchedInPreviousSeasonsInteractor.kt │ │ ├── SeasonDetailsInteractor.kt │ │ └── model/ │ │ ├── SeasonCast.kt │ │ ├── SeasonDetailsResult.kt │ │ └── SeasonImages.kt │ ├── settings/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── settings/ │ │ └── ObserveSettingsPreferencesInteractor.kt │ ├── showdetails/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── showdetails/ │ │ ├── FollowShowInteractor.kt │ │ ├── Mapper.kt │ │ ├── ObservableShowDetailsInteractor.kt │ │ ├── ShowContentSyncInteractor.kt │ │ ├── ShowDetailsInteractor.kt │ │ └── model/ │ │ └── ShowDetails.kt │ ├── similarshows/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── similarshows/ │ │ └── SimilarShowsInteractor.kt │ ├── theme/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── theme/ │ │ ├── ImageQuality.kt │ │ └── Theme.kt │ ├── traktlists/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── traktlists/ │ │ ├── CreateTraktListInteractor.kt │ │ ├── ObserveTraktListsInteractor.kt │ │ ├── SyncTraktListsInteractor.kt │ │ └── ToggleShowInListInteractor.kt │ ├── upnext/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── domain/ │ │ │ └── upnext/ │ │ │ ├── ObserveUpNextInteractor.kt │ │ │ ├── RefreshUpNextInteractor.kt │ │ │ ├── UpNextSyncWorker.kt │ │ │ ├── UpNextTasksInitializer.kt │ │ │ ├── di/ │ │ │ │ └── UpNextTasksInitializerBindingContainer.kt │ │ │ └── model/ │ │ │ ├── UpNextResult.kt │ │ │ └── UpNextSortOption.kt │ │ └── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── upnext/ │ │ └── ObserveUpNextInteractorTest.kt │ ├── user/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── user/ │ │ ├── ObserveUserProfileInteractor.kt │ │ ├── UpdateUserProfileData.kt │ │ └── model/ │ │ ├── UserProfile.kt │ │ └── UserStats.kt │ ├── watchlist/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── domain/ │ │ │ └── watchlist/ │ │ │ ├── ObservableWatchlistInteractor.kt │ │ │ ├── ObserveUpNextSectionsInteractor.kt │ │ │ ├── ObserveWatchlistSectionsInteractor.kt │ │ │ ├── UpNextSectionsMapper.kt │ │ │ ├── WatchlistSyncInteractor.kt │ │ │ └── model/ │ │ │ ├── EpisodeBadge.kt │ │ │ ├── UpNextSections.kt │ │ │ └── WatchlistSections.kt │ │ └── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── domain/ │ │ └── watchlist/ │ │ ├── ObservableWatchlistInteractorTest.kt │ │ ├── ObserveUpNextSectionsInteractorTest.kt │ │ ├── ObserveWatchlistSectionsInteractorTest.kt │ │ └── UpNextSectionsMapperTest.kt │ └── watchproviders/ │ ├── build.gradle.kts │ └── src/ │ └── commonMain/ │ └── kotlin/ │ └── com/ │ └── thomaskioko/ │ └── tvmaniac/ │ └── domain/ │ └── watchproviders/ │ └── WatchProvidersInteractor.kt ├── fastlane/ │ ├── Appfile │ ├── Fastfile │ ├── Matchfile │ ├── Pluginfile │ └── README.md ├── features/ │ ├── calendar/ │ │ ├── nav/ │ │ │ └── build.gradle.kts │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── presentation/ │ │ │ │ └── calendar/ │ │ │ │ ├── CalendarAction.kt │ │ │ │ ├── CalendarPresenter.kt │ │ │ │ ├── CalendarState.kt │ │ │ │ ├── CalendarStateMapper.kt │ │ │ │ └── model/ │ │ │ │ ├── CalendarDateGroup.kt │ │ │ │ └── CalendarEpisodeItem.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── presentation/ │ │ │ └── calendar/ │ │ │ └── CalendarPresenterTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── ui/ │ │ │ └── calendar/ │ │ │ └── CalendarScreen.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── ui/ │ │ └── calendar/ │ │ └── roborrazi/ │ │ └── CalendarScreenshotTest.kt │ ├── debug/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── debug/ │ │ │ └── nav/ │ │ │ └── DebugRoute.kt │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── debug/ │ │ │ │ └── presenter/ │ │ │ │ ├── DebugActions.kt │ │ │ │ ├── DebugPresenter.kt │ │ │ │ └── DebugState.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── debug/ │ │ │ └── presenter/ │ │ │ └── DebugPresenterTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── debug/ │ │ └── ui/ │ │ └── DebugMenuScreen.kt │ ├── discover/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── discover/ │ │ │ └── nav/ │ │ │ └── DiscoverNavigator.kt │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── discover/ │ │ │ │ └── presenter/ │ │ │ │ ├── DiscoverShowsAction.kt │ │ │ │ ├── DiscoverShowsMapper.kt │ │ │ │ ├── DiscoverShowsPresenter.kt │ │ │ │ ├── DiscoverViewState.kt │ │ │ │ ├── di/ │ │ │ │ │ └── DefaultDiscoverNavigator.kt │ │ │ │ └── model/ │ │ │ │ ├── DiscoverShow.kt │ │ │ │ └── NextEpisodeUiModel.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── discover/ │ │ │ └── presenter/ │ │ │ └── DiscoverShowsPresenterTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ ├── lint-baseline.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── discover/ │ │ │ └── ui/ │ │ │ ├── DiscoverPreviewParameterProvider.kt │ │ │ ├── DiscoverScreen.kt │ │ │ └── component/ │ │ │ ├── CircularIndicator.kt │ │ │ ├── DiscoverHeaderContent.kt │ │ │ ├── HorizontalRowContent.kt │ │ │ ├── NextEpisodeCard.kt │ │ │ └── NextEpisodesSection.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── discover/ │ │ └── roborrazi/ │ │ └── DiscoverScreenshotTest.kt │ ├── episode-sheet/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── espisodedetails/ │ │ │ └── nav/ │ │ │ └── model/ │ │ │ ├── EpisodeSheetConfig.kt │ │ │ ├── ScreenSource.kt │ │ │ └── SheetNavigatorExt.kt │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── presentation/ │ │ │ │ └── episodedetail/ │ │ │ │ ├── EpisodeSheetAction.kt │ │ │ │ ├── EpisodeSheetMapper.kt │ │ │ │ ├── EpisodeSheetPresenter.kt │ │ │ │ └── EpisodeSheetState.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── presentation/ │ │ │ └── episodedetail/ │ │ │ └── EpisodeSheetPresenterTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── episodedetail/ │ │ │ └── ui/ │ │ │ ├── EpisodeDetailBottomSheet.kt │ │ │ └── EpisodeSheet.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── episodedetail/ │ │ └── roborrazi/ │ │ └── EpisodeSheetScreenshotTest.kt │ ├── genre-shows/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── genreshows/ │ │ │ └── nav/ │ │ │ ├── GenreShowsDestination.kt │ │ │ └── GenreShowsRoute.kt │ │ └── presenter/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── genreshows/ │ │ └── presenter/ │ │ └── di/ │ │ └── GenreShowsNavDestinationBinding.kt │ ├── home/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── home/ │ │ │ └── nav/ │ │ │ ├── HomeRoute.kt │ │ │ ├── HomeTabNavigator.kt │ │ │ ├── TabChild.kt │ │ │ ├── TabDestination.kt │ │ │ └── di/ │ │ │ ├── TabDestinationMultibindings.kt │ │ │ └── model/ │ │ │ └── HomeConfig.kt │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── presenter/ │ │ │ │ └── home/ │ │ │ │ ├── HomePresenter.kt │ │ │ │ └── di/ │ │ │ │ └── DefaultHomeTabNavigator.kt │ │ │ ├── commonTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── presenter/ │ │ │ │ └── home/ │ │ │ │ └── HomePresenterTest.kt │ │ │ ├── iosTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── presenter/ │ │ │ │ └── home/ │ │ │ │ └── HomePresenterIosTest.kt │ │ │ └── jvmTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── presenter/ │ │ │ └── home/ │ │ │ └── HomePresenterJvmTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── home/ │ │ └── ui/ │ │ └── HomeScreen.kt │ ├── library/ │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── presentation/ │ │ │ └── library/ │ │ │ ├── LibraryAction.kt │ │ │ ├── LibraryPresenter.kt │ │ │ ├── LibraryState.kt │ │ │ └── model/ │ │ │ ├── LibraryShowItem.kt │ │ │ ├── LibrarySortOption.kt │ │ │ └── ShowStatus.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── ui/ │ │ └── library/ │ │ ├── LibraryListItem.kt │ │ ├── LibraryScreen.kt │ │ ├── LibrarySearchbar.kt │ │ ├── SortOptionsContent.kt │ │ └── preview/ │ │ ├── LibraryListItemPreviewParameterProvider.kt │ │ └── LibraryStatePreviewParameterProvider.kt │ ├── more-shows/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── moreshows/ │ │ │ └── nav/ │ │ │ └── MoreShowsRoute.kt │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── moreshows/ │ │ │ └── presentation/ │ │ │ ├── MoreShowsAction.kt │ │ │ ├── MoreShowsPresenter.kt │ │ │ ├── MoreShowsState.kt │ │ │ └── TvShow.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── moreshows/ │ │ │ └── ui/ │ │ │ ├── MoreShowsPreviewParameterProvider.kt │ │ │ └── MoreShowsScreen.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── moreshows/ │ │ └── roborrazi/ │ │ └── MoreShowsScreenTest.kt │ ├── profile/ │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── profile/ │ │ │ │ └── presenter/ │ │ │ │ ├── ProfileAction.kt │ │ │ │ ├── ProfilePresenter.kt │ │ │ │ └── model/ │ │ │ │ ├── ProfileInfo.kt │ │ │ │ ├── ProfileState.kt │ │ │ │ └── ProfileStats.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── presenter/ │ │ │ └── profile/ │ │ │ └── ProfilePresenterTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── profile/ │ │ │ └── ui/ │ │ │ ├── ProfilePreviewParameterProvider.kt │ │ │ ├── ProfileScreen.kt │ │ │ ├── StatsCardItem.kt │ │ │ └── UnauthenticatedContent.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── profile/ │ │ └── roborazzi/ │ │ └── ProfileScreenTest.kt │ ├── progress/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── progress/ │ │ │ └── nav/ │ │ │ └── scope/ │ │ │ └── ProgressChildScope.kt │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── presentation/ │ │ │ └── progress/ │ │ │ ├── ProgressAction.kt │ │ │ ├── ProgressChildGraph.kt │ │ │ ├── ProgressPresenter.kt │ │ │ └── ProgressState.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── ui/ │ │ │ └── progress/ │ │ │ └── ProgressScreen.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── ui/ │ │ └── progress/ │ │ └── roborrazi/ │ │ └── ProgressScreenshotTest.kt │ ├── root/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── root/ │ │ │ ├── model/ │ │ │ │ ├── DeepLinkDestination.kt │ │ │ │ ├── NotificationPermissionState.kt │ │ │ │ └── ThemeState.kt │ │ │ └── nav/ │ │ │ └── NotificationRationale.kt │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── presenter/ │ │ │ │ └── root/ │ │ │ │ ├── DefaultRootPresenter.kt │ │ │ │ ├── RootPresenter.kt │ │ │ │ └── di/ │ │ │ │ ├── DefaultNotificationRationale.kt │ │ │ │ ├── DefaultSheetNavigator.kt │ │ │ │ └── RootPresenterBindingContainer.kt │ │ │ ├── commonTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── presenter/ │ │ │ │ └── root/ │ │ │ │ └── DefaultRootPresenterTest.kt │ │ │ ├── iosTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── presenter/ │ │ │ │ └── root/ │ │ │ │ └── DefaultRootPresenterIosTest.kt │ │ │ └── jvmTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── presenter/ │ │ │ └── root/ │ │ │ └── DefaultRootPresenterJvmTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── app/ │ │ └── ui/ │ │ └── RootScreen.kt │ ├── search/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── search/ │ │ │ └── nav/ │ │ │ └── SearchRoute.kt │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── search/ │ │ │ │ └── presenter/ │ │ │ │ ├── Mapper.kt │ │ │ │ ├── SearchShowAction.kt │ │ │ │ ├── SearchShowState.kt │ │ │ │ ├── SearchShowsPresenter.kt │ │ │ │ └── model/ │ │ │ │ ├── CategoryItem.kt │ │ │ │ ├── GenreRowModel.kt │ │ │ │ ├── ShowGenre.kt │ │ │ │ └── ShowItem.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── presenter/ │ │ │ └── search/ │ │ │ └── SearchShowsPresenterTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── search/ │ │ │ └── ui/ │ │ │ ├── SearchPreviewParameterProvider.kt │ │ │ ├── SearchScreen.kt │ │ │ └── components/ │ │ │ ├── HorizontalShowContentRow.kt │ │ │ └── SearchResultItem.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── search/ │ │ └── roborrazi/ │ │ └── SearchScreenTest.kt │ ├── season-details/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── seasondetails/ │ │ │ └── nav/ │ │ │ ├── SeasonDetailsRoute.kt │ │ │ └── SeasonDetailsUiParam.kt │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── seasondetails/ │ │ │ │ └── presenter/ │ │ │ │ ├── Mapper.kt │ │ │ │ ├── SeasonDetailsAction.kt │ │ │ │ ├── SeasonDetailsModel.kt │ │ │ │ ├── SeasonDetailsPresenter.kt │ │ │ │ └── model/ │ │ │ │ ├── Cast.kt │ │ │ │ ├── EpisodeDetailsModel.kt │ │ │ │ └── SeasonImagesModel.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── seasondetails/ │ │ │ └── presenter/ │ │ │ ├── SeasonPresenterTest.kt │ │ │ └── data/ │ │ │ └── MockData.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── seasondetails/ │ │ │ └── ui/ │ │ │ ├── SeasonDetailsScreen.kt │ │ │ ├── SeasonPreviewParameterProvider.kt │ │ │ └── components/ │ │ │ ├── CollapsableContent.kt │ │ │ └── EpisodeItem.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── seasondetails/ │ │ └── roborrazi/ │ │ └── SeasonScreenshotTest.kt │ ├── settings/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── settings/ │ │ │ └── nav/ │ │ │ └── SettingsRoute.kt │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── settings/ │ │ │ │ └── presenter/ │ │ │ │ ├── SettingsActions.kt │ │ │ │ ├── SettingsPresenter.kt │ │ │ │ ├── SettingsState.kt │ │ │ │ └── ThemeModel.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── presenter/ │ │ │ └── settings/ │ │ │ └── SettingsPresenterTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── settings/ │ │ │ │ └── ui/ │ │ │ │ ├── AboutSheetContent.kt │ │ │ │ ├── SettingsPreviewParameterProvider.kt │ │ │ │ ├── SettingsScreen.kt │ │ │ │ ├── ThemePreviewSwatch.kt │ │ │ │ └── ThemeSelectorSection.kt │ │ │ └── res/ │ │ │ └── drawable/ │ │ │ └── ic_app_launcher.xml │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── seasondetails/ │ │ └── roborrazi/ │ │ └── SettingsScreenshotTest.kt │ ├── show-details/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── showdetails/ │ │ │ └── nav/ │ │ │ ├── ShowDetailsRoute.kt │ │ │ └── model/ │ │ │ ├── ShowDetailsParam.kt │ │ │ └── ShowSeasonDetailsParam.kt │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── presenter/ │ │ │ │ └── showdetails/ │ │ │ │ ├── ShowDetailsAction.kt │ │ │ │ ├── ShowDetailsContent.kt │ │ │ │ ├── ShowDetailsMapper.kt │ │ │ │ ├── ShowDetailsPresenter.kt │ │ │ │ └── model/ │ │ │ │ ├── CastModel.kt │ │ │ │ ├── ContinueTrackingEpisodeModel.kt │ │ │ │ ├── ProviderModel.kt │ │ │ │ ├── SeasonModel.kt │ │ │ │ ├── ShowDetailsModel.kt │ │ │ │ ├── ShowModel.kt │ │ │ │ ├── TrailerModel.kt │ │ │ │ └── TraktListModel.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── presenter/ │ │ │ └── showdetails/ │ │ │ ├── MockData.kt │ │ │ └── ShowDetailsPresenterTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── showdetails/ │ │ │ └── ui/ │ │ │ ├── DetailPreviewParameterProvider.kt │ │ │ ├── ShowDetailScreen.kt │ │ │ └── components/ │ │ │ ├── ContinueTrackingCard.kt │ │ │ ├── ContinueTrackingSection.kt │ │ │ ├── SeasonChipItem.kt │ │ │ ├── ShowListSheetContent.kt │ │ │ └── WatchProgressSection.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── showdetails/ │ │ └── roborrazi/ │ │ ├── ShowDetailsScreenScreenshotTest.kt │ │ └── ShowListSheetScreenshotTest.kt │ ├── trailers/ │ │ ├── nav/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── trailers/ │ │ │ └── nav/ │ │ │ └── TrailersRoute.kt │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── presenter/ │ │ │ │ └── trailers/ │ │ │ │ ├── Mapper.kt │ │ │ │ ├── TrailersAction.kt │ │ │ │ ├── TrailersPresenter.kt │ │ │ │ ├── TrailersState.kt │ │ │ │ └── model/ │ │ │ │ └── Trailer.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── presenter/ │ │ │ └── trailers/ │ │ │ └── TrailersPresenterTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── trailers/ │ │ └── ui/ │ │ ├── TrailerPreviewParameterProvider.kt │ │ └── TrailersScreen.kt │ ├── upnext/ │ │ ├── presenter/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── thomaskioko/ │ │ │ │ └── tvmaniac/ │ │ │ │ └── presentation/ │ │ │ │ └── upnext/ │ │ │ │ ├── UpNextAction.kt │ │ │ │ ├── UpNextPresenter.kt │ │ │ │ ├── UpNextState.kt │ │ │ │ └── model/ │ │ │ │ └── UpNextEpisodeUiModel.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── presentation/ │ │ │ └── upnext/ │ │ │ └── UpNextPresenterTest.kt │ │ └── ui/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── ui/ │ │ │ └── upnext/ │ │ │ ├── UpNextListItem.kt │ │ │ ├── UpNextScreen.kt │ │ │ └── preview/ │ │ │ └── UpNextPreviewParameterProvider.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── ui/ │ │ └── upnext/ │ │ └── roborrazi/ │ │ └── UpNextScreenshotTest.kt │ └── watchlist/ │ ├── presenter/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── watchlist/ │ │ │ └── presenter/ │ │ │ ├── Mapper.kt │ │ │ ├── WatchlistAction.kt │ │ │ ├── WatchlistPresenter.kt │ │ │ ├── WatchlistState.kt │ │ │ └── model/ │ │ │ ├── EpisodeBadge.kt │ │ │ ├── NextEpisodeItem.kt │ │ │ ├── SectionedEpisodes.kt │ │ │ ├── SectionedItems.kt │ │ │ ├── UpNextEpisodeItem.kt │ │ │ └── WatchlistItem.kt │ │ └── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ ├── domain/ │ │ │ └── watchlist/ │ │ │ ├── MockData.kt │ │ │ └── WatchlistPresenterTest.kt │ │ └── watchlist/ │ │ └── presenter/ │ │ └── FakeWatchlistPresenterBuilder.kt │ └── ui/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── ui/ │ │ └── library/ │ │ ├── WatchListUpNextListItem.kt │ │ ├── WatchlistListItem.kt │ │ ├── WatchlistPreviewParameterProvider.kt │ │ ├── WatchlistScreen.kt │ │ └── component/ │ │ └── Searchbar.kt │ └── test/ │ └── kotlin/ │ └── com/ │ └── thomaskioko/ │ └── tvmaniac/ │ └── watchlist/ │ └── roborrazi/ │ └── WatchlistScreenTest.kt ├── gradle/ │ ├── gradle-daemon-jvm.properties │ ├── libs.versions.toml │ ├── lint.xml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── i18n/ │ ├── api/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── i18n/ │ │ └── api/ │ │ └── Localizer.kt │ ├── generator/ │ │ ├── build.gradle.kts │ │ ├── lint-baseline.xml │ │ └── src/ │ │ ├── androidMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── i18n/ │ │ │ └── Resources.kt │ │ ├── commonMain/ │ │ │ └── moko-resources/ │ │ │ ├── base/ │ │ │ │ ├── plurals.xml │ │ │ │ └── strings.xml │ │ │ ├── de/ │ │ │ │ ├── plurals.xml │ │ │ │ └── strings.xml │ │ │ └── fr/ │ │ │ ├── plurals.xml │ │ │ └── strings.xml │ │ ├── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── i18n/ │ │ │ └── generator/ │ │ │ └── ResourceTest.kt │ │ └── iosMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── i18n/ │ │ └── Resources.kt │ ├── implementation/ │ │ ├── build.gradle.kts │ │ ├── lint-baseline.xml │ │ └── src/ │ │ ├── androidHostTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── i18n/ │ │ │ └── util/ │ │ │ └── BaseResourceTests.kt │ │ ├── androidMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── i18n/ │ │ │ └── PlatformLocalizer.android.kt │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── i18n/ │ │ │ ├── LocalizedErrorToStringMapper.kt │ │ │ ├── MokoLocaleInitializer.kt │ │ │ ├── MokoResourcesLocalizer.kt │ │ │ ├── PlatformLocalizer.kt │ │ │ └── di/ │ │ │ └── MokoLocaleInitializerBindingContainer.kt │ │ ├── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── i18n/ │ │ │ ├── LocalizedStringTest.kt │ │ │ ├── MokoLocalizerTest.kt │ │ │ └── util/ │ │ │ └── BaseResourceTests.kt │ │ ├── iosMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── i18n/ │ │ │ └── PlatformLocalizer.ios.kt │ │ ├── iosTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── i18n/ │ │ │ └── util/ │ │ │ └── BaseResourceTests.kt │ │ ├── jvmMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── i18n/ │ │ │ └── PlatformLocalizer.jvm.kt │ │ └── jvmTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── i18n/ │ │ └── util/ │ │ └── BaseResourceTests.kt │ └── testing/ │ ├── build.gradle.kts │ └── src/ │ ├── androidMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── i18n/ │ │ └── testing/ │ │ └── util/ │ │ ├── BaseLocalizerTest.android.kt │ │ └── StringDescExt.kt │ ├── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── i18n/ │ │ └── testing/ │ │ ├── FakeLocalizer.kt │ │ └── util/ │ │ ├── BaseLocalizerTest.kt │ │ └── StringDescExt.kt │ ├── iosMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── i18n/ │ │ └── testing/ │ │ └── util/ │ │ ├── BaseLocalizerTest.ios.kt │ │ └── StringDescExt.ios.kt │ └── jvmMain/ │ └── kotlin/ │ └── com/ │ └── thomaskioko/ │ └── tvmaniac/ │ └── i18n/ │ └── testing/ │ └── util/ │ ├── BaseLocalizerTest.jvm.kt │ └── StringDescExt.jvm.kt ├── ios/ │ ├── .gitignore │ ├── .swiftformat │ ├── Config/ │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Modules/ │ │ ├── CoreKit/ │ │ │ ├── Package.swift │ │ │ └── Sources/ │ │ │ └── CoreKit/ │ │ │ ├── CoreLogger.swift │ │ │ ├── DefaultDiagnosticLogger.swift │ │ │ ├── DiagnosticLogger.swift │ │ │ ├── FirebaseCrashlyticsBridge.swift │ │ │ ├── ImageCacheManager.swift │ │ │ ├── MemoryMonitor.swift │ │ │ └── SystemMemory.swift │ │ ├── SnapshotTestingLib/ │ │ │ ├── .gitignore │ │ │ ├── .swiftpm/ │ │ │ │ └── xcode/ │ │ │ │ └── package.xcworkspace/ │ │ │ │ └── xcshareddata/ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ ├── Package.swift │ │ │ └── Sources/ │ │ │ └── SnapshotTestingLib/ │ │ │ └── SnapshotTesting+Extensions.swift │ │ ├── SwiftUIComponents/ │ │ │ ├── .gitignore │ │ │ ├── .swiftpm/ │ │ │ │ └── xcode/ │ │ │ │ └── package.xcworkspace/ │ │ │ │ └── xcshareddata/ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ ├── Package.swift │ │ │ ├── Sources/ │ │ │ │ └── SwiftUIComponents/ │ │ │ │ ├── Components/ │ │ │ │ │ ├── BorderTextView.swift │ │ │ │ │ ├── BottomSheet/ │ │ │ │ │ │ └── EpisodeDetailSheetContent.swift │ │ │ │ │ ├── Buttons/ │ │ │ │ │ │ ├── CircularButton.swift │ │ │ │ │ │ ├── FilledImageButton.swift │ │ │ │ │ │ ├── OutlinedButton.swift │ │ │ │ │ │ ├── RoundedButton.swift │ │ │ │ │ │ └── TvManiacButton.swift │ │ │ │ │ ├── CarouselView.swift │ │ │ │ │ ├── CastListView.swift │ │ │ │ │ ├── ChevronTitle.swift │ │ │ │ │ ├── ChipView.swift │ │ │ │ │ ├── CircularIndicator.swift │ │ │ │ │ ├── ContinueTracking/ │ │ │ │ │ │ ├── ContinueTrackingCard.swift │ │ │ │ │ │ ├── ContinueTrackingSection.swift │ │ │ │ │ │ └── SwiftContinueTrackingEpisode.swift │ │ │ │ │ ├── EmptyUIView.swift │ │ │ │ │ ├── Episode/ │ │ │ │ │ │ ├── EpisodeCollapsible.swift │ │ │ │ │ │ ├── EpisodeItemView.swift │ │ │ │ │ │ └── EpisodeListView.swift │ │ │ │ │ ├── FilterChip.swift │ │ │ │ │ ├── FilterChipSection.swift │ │ │ │ │ ├── FlowLayout.swift │ │ │ │ │ ├── FullScreenView.swift │ │ │ │ │ ├── GlassButton.swift │ │ │ │ │ ├── GlassToolbar.swift │ │ │ │ │ ├── GridView.swift │ │ │ │ │ ├── HorizontalItemListView.swift │ │ │ │ │ ├── ImageGalleryContentView.swift │ │ │ │ │ ├── Images/ │ │ │ │ │ │ ├── AvatarView.swift │ │ │ │ │ │ ├── BackdropPosterCard.swift │ │ │ │ │ │ ├── CastCardView.swift │ │ │ │ │ │ ├── FeaturedContentPosterView.swift │ │ │ │ │ │ ├── HeaderCoverArtWorkView.swift │ │ │ │ │ │ ├── LazyResizableImage.swift │ │ │ │ │ │ ├── PosterCardView.swift │ │ │ │ │ │ ├── PosterItemView.swift │ │ │ │ │ │ ├── PosterPlaceholder.swift │ │ │ │ │ │ ├── ProviderItemView.swift │ │ │ │ │ │ └── TransparentImageBackground.swift │ │ │ │ │ ├── LibraryListItemView.swift │ │ │ │ │ ├── LoadingIndicatorView.swift │ │ │ │ │ ├── Models/ │ │ │ │ │ │ ├── DebugMenuItem.swift │ │ │ │ │ │ ├── SettingsModels.swift │ │ │ │ │ │ ├── ShowPosterImage.swift │ │ │ │ │ │ ├── SwiftCalendarDateGroup.swift │ │ │ │ │ │ ├── SwiftCast.swift │ │ │ │ │ │ ├── SwiftGenreRow.swift │ │ │ │ │ │ ├── SwiftGenres.swift │ │ │ │ │ │ ├── SwiftLibraryItem.swift │ │ │ │ │ │ ├── SwiftProfile.swift │ │ │ │ │ │ ├── SwiftProviders.swift │ │ │ │ │ │ ├── SwiftSearchShow.swift │ │ │ │ │ │ ├── SwiftSeason.swift │ │ │ │ │ │ ├── SwiftShow.swift │ │ │ │ │ │ ├── SwiftShowGenre.swift │ │ │ │ │ │ ├── SwiftTrailer.swift │ │ │ │ │ │ └── SwiftTraktListItem.swift │ │ │ │ │ ├── NavigationTopBar.swift │ │ │ │ │ ├── NextEpisode/ │ │ │ │ │ │ ├── NextEpisodeCard.swift │ │ │ │ │ │ ├── NextEpisodesSection.swift │ │ │ │ │ │ ├── SwiftNextEpisode.swift │ │ │ │ │ │ └── UpNextListItemView.swift │ │ │ │ │ ├── NotificationRationaleSheet.swift │ │ │ │ │ ├── OverviewBoxView.swift │ │ │ │ │ ├── ParallaxView.swift │ │ │ │ │ ├── ProviderListView.swift │ │ │ │ │ ├── ScanlineOverlay.swift │ │ │ │ │ ├── Search/ │ │ │ │ │ │ ├── SearchItemView.swift │ │ │ │ │ │ ├── SearchResultListView.swift │ │ │ │ │ │ └── ShowContent/ │ │ │ │ │ │ ├── HorizontalShowContentView.swift │ │ │ │ │ │ └── ShowContentItemView.swift │
Copy disabled (too large)
Download .json
Condensed preview — 1716 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (10,518K chars).
[
{
"path": ".editorconfig",
"chars": 832,
"preview": "root = true\n\n[*]\ncharset = utf-8\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespa"
},
{
"path": ".geminiignore",
"chars": 209,
"preview": ".gradle/\nbuild/\n.kotlin/\n.idea/\n.build/\nios/build/\nios/SourcePackages/\nderived_data/\n*.log\n*.class\n*.apk\n*.ap_\n*.dex\nloc"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 2478,
"preview": "name: Bug Report\ndescription: Report a reproducible bug in TvManiac.\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nbody:\n - type: ma"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 28,
"preview": "blank_issues_enabled: false\n"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 1618,
"preview": "name: Feature Request\ndescription: Suggest a new feature or improvement for TvManiac.\ntitle: \"[Feature]: \"\nlabels: [\"enh"
},
{
"path": ".github/actions/setup-android-release/action.yml",
"chars": 1642,
"preview": "name: 'Setup Android Release'\ndescription: 'Common setup for Android release builds: Gradle, Ruby, google-services.json,"
},
{
"path": ".github/actions/setup-gradle/action.yml",
"chars": 1155,
"preview": "name: 'Setup Gradle Environment'\ndescription: 'Common setup for Gradle builds with JDK 21 and environment variables'\n\nin"
},
{
"path": ".github/actions/setup-ios/action.yml",
"chars": 1202,
"preview": "name: 'Setup iOS Environment'\ndescription: 'Common setup for iOS builds with Xcode, Ruby, SPM cache, and Gradle'\n\ninputs"
},
{
"path": ".github/actions/setup-ios-release/action.yml",
"chars": 990,
"preview": "name: 'Setup iOS Release'\ndescription: 'Common setup for iOS release builds: Xcode, Ruby, SPM cache, Gradle, and GoogleS"
},
{
"path": ".github/release.yml",
"chars": 528,
"preview": "changelog:\n exclude:\n labels:\n - skip-changelog\n authors:\n - renovate[bot]\n - dependabot[bot]\n ca"
},
{
"path": ".github/renovate.json",
"chars": 1702,
"preview": "{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\n \"config:recommended\",\n "
},
{
"path": ".github/workflows/baseline-profile.yml",
"chars": 1817,
"preview": "name: Weekly Baseline Profile Generation\n\non:\n schedule:\n - cron: '30 0 * * 0,3,5' #Every Sunday, Wednesday, and Fri"
},
{
"path": ".github/workflows/beta-release.yml",
"chars": 5092,
"preview": "name: Beta Release\n\non:\n workflow_dispatch:\n inputs:\n skip_android:\n description: 'Skip Android build'\n "
},
{
"path": ".github/workflows/ci.yml",
"chars": 7202,
"preview": "name: build\n\non:\n push:\n branches: [ main ]\n pull_request:\n types: [ opened, synchronize ]\n workf"
},
{
"path": ".github/workflows/compare-screenshot.yml",
"chars": 5726,
"preview": "name: Compare Screenshot\n\non:\n pull_request:\n\npermissions: {}\n\nenv:\n TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}\n TRAKT"
},
{
"path": ".github/workflows/daily-build.yml",
"chars": 4902,
"preview": "name: Daily Build\n\non:\n workflow_dispatch:\n # Uncomment to enable scheduled daily builds\n # schedule:\n # - cron: '"
},
{
"path": ".github/workflows/nightly-integration-tests.yml",
"chars": 2480,
"preview": "name: Nightly Integration Tests\n\non:\n schedule:\n - cron: '30 1 * * *'\n workflow_dispatch:\n workflow_call:\n\nconcurr"
},
{
"path": ".github/workflows/promote-release.yml",
"chars": 9478,
"preview": "name: Promote Release\n\non:\n # schedule:\n # - cron: '0 5 * * *'\n workflow_dispatch:\n inputs:\n android_rollou"
},
{
"path": ".github/workflows/release.yml",
"chars": 6336,
"preview": "name: Release\n\non:\n push:\n tags:\n - 'v[0-9]+.[0-9]+.[0-9]+'\n\npermissions:\n contents: write\n\nenv:\n XCODE_VERSI"
},
{
"path": ".github/workflows/store-screenshot.yml",
"chars": 1423,
"preview": "name: Store Screenshot\n\non:\n push:\n branches:\n - main\n\npermissions: {}\n\nenv:\n TMDB_API_KEY: ${{ secrets.TMDB_A"
},
{
"path": ".gitignore",
"chars": 1635,
"preview": "# Built application files\n*.apk\n*.ap_\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated file"
},
{
"path": ".idea/codeStyles/Project.xml",
"chars": 5383,
"preview": "<component name=\"ProjectCodeStyleConfiguration\">\n <code_scheme name=\"Project\" version=\"173\">\n <JavaCodeStyleSettings"
},
{
"path": ".idea/codeStyles/codeStyleConfig.xml",
"chars": 151,
"preview": "<component name=\"ProjectCodeStyleConfiguration\">\n <state>\n <option name=\"PREFERRED_PROJECT_CODE_STYLE\" value=\"tv-man"
},
{
"path": ".idea/dictionaries/project.xml",
"chars": 163,
"preview": "<component name=\"ProjectDictionaryState\">\n <dictionary name=\"project\">\n <words>\n <w>tmdb</w>\n <w>trakt</w>"
},
{
"path": ".ruby-version",
"chars": 6,
"preview": "3.3.0\n"
},
{
"path": ".swiftformat",
"chars": 177,
"preview": "--indent 4\n--exclude ios/build,ios/Modules/*/.build,**/.build,**/build,**/ios-framework/build,ios/SourcePackages,ios/der"
},
{
"path": ".swiftlint.yml",
"chars": 715,
"preview": "cyclomatic_complexity:\n warning: 15\n error: 20\n\nfunction_body_length:\n warning: 75\n error: 100\n\ndisabled_rul"
},
{
"path": "CHANGELOG.md",
"chars": 1007,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## [0.1.2] - 2026-03-25\n\n### Bug Fixe"
},
{
"path": "CONTRIBUTING.md",
"chars": 4088,
"preview": "# Contributing to TvManiac\n\nTvManiac is a personal learning playground for Kotlin Multiplatform development. Contributio"
},
{
"path": "GEMINI.md",
"chars": 5303,
"preview": "# TV Maniac Agent Rules\n\n## Project Overview\nTV Maniac is a Kotlin Multiplatform (KMP) project for tracking TV shows. It"
},
{
"path": "Gemfile",
"chars": 213,
"preview": "source \"https://rubygems.org\"\n\ngem \"fastlane\"\ngem \"xcode-install\"\ngem \"xcpretty\"\n\nplugins_path = File.join(File.dirname("
},
{
"path": "LICENSE",
"chars": 11324,
"preview": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licen"
},
{
"path": "README.md",
"chars": 7104,
"preview": "<p align=\"center\">\n<img src=\"art/TvManiacBanner.png\" width=\"100%\" />\n</p>\n\n# TvManiac\n\n }\n\nscaffold {\n android {\n enableAndroidResources()\n\n useCompo"
},
{
"path": "android-designsystem/src/debug/res/values/strings.xml",
"chars": 131,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <string name=\"app_name\" translatable=\"false\">TvManiac</string>\n</"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Background.kt",
"chars": 1958,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport android.content.res.Configuration\nimport androidx.compose.fo"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/BadgeChip.kt",
"chars": 1890,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.c"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Buttons.kt",
"chars": 14717,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.animation.Crossfade\nimport androidx.compose"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Card.kt",
"chars": 11456,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.background\nimport androidx.compo"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Chip.kt",
"chars": 1967,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.c"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Dialogs.kt",
"chars": 4325,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.c"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/EmptyLayout.kt",
"chars": 3318,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androi"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/ErrorLayout.kt",
"chars": 3999,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.animation.animateColorAsState\nimport androi"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/FilterChipSection.kt",
"chars": 8460,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.animation.animateContentSize\nimport android"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/GradientScrim.kt",
"chars": 1569,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport android.annotation.SuppressLint\nimport androidx.compose.runt"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Image.kt",
"chars": 8098,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.com"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/NavigationBar.kt",
"chars": 4214,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.layout.RowScope\nimport androidx."
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/NotificationRationaleContent.kt",
"chars": 6988,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.background\nimport androidx.compo"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/PosterPlaceholder.kt",
"chars": 3288,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.background\nimport androidx.compo"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/ProgressIndicator.kt",
"chars": 1102,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compo"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/ScanlineOverlay.kt",
"chars": 2560,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.f"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/SearchTextField.kt",
"chars": 7603,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.gestures.detectTapGestures\nimpor"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/SegmentedProgressBar.kt",
"chars": 2841,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.background\nimport androidx.compo"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/SheetDragHandle.kt",
"chars": 3287,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.background\nimport androidx.compo"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/ShowLinearProgressIndicator.kt",
"chars": 1177,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.material3.LinearProgressIndicator\nimport an"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Snackbar.kt",
"chars": 10583,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.animation.AnimatedVisibility\nimport android"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/Text.kt",
"chars": 5742,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.clickable\nimport androidx.compos"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/TextTitlePill.kt",
"chars": 2505,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.com"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/TopBar.kt",
"chars": 12847,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport android.annotation.SuppressLint\nimport androidx.compose.anim"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/TvManiacBottomSheet.kt",
"chars": 3506,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.foundation.layout.ColumnScope\nimport androi"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/components/TvManiacPreviewWrapperProvider.kt",
"chars": 568,
"preview": "package com.thomaskioko.tvmaniac.compose.components\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose."
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/extensions/GradientExtensions.kt",
"chars": 1104,
"preview": "package com.thomaskioko.tvmaniac.compose.extensions\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.com"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/extensions/LazyListExtensions.kt",
"chars": 1280,
"preview": "package com.thomaskioko.tvmaniac.compose.extensions\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose."
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/extensions/PaddingValuesExtentions.kt",
"chars": 1085,
"preview": "package com.thomaskioko.tvmaniac.compose.extensions\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport andr"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/extensions/ScrimExtentions.kt",
"chars": 3208,
"preview": "package com.thomaskioko.tvmaniac.compose.extensions\n\nimport android.annotation.SuppressLint\nimport androidx.annotation.F"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/theme/Background.kt",
"chars": 584,
"preview": "package com.thomaskioko.tvmaniac.compose.theme\n\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtim"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/theme/Colors.kt",
"chars": 6623,
"preview": "package com.thomaskioko.tvmaniac.compose.theme\n\nimport androidx.compose.ui.graphics.Color\n\npublic val green: Color = Col"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/theme/Shape.kt",
"chars": 341,
"preview": "package com.thomaskioko.tvmaniac.compose.theme\n\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport andro"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/theme/Theme.kt",
"chars": 6766,
"preview": "package com.thomaskioko.tvmaniac.compose.theme\n\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.c"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/theme/Type.kt",
"chars": 4150,
"preview": "package com.thomaskioko.tvmaniac.compose.theme\n\nimport androidx.compose.material3.Typography\nimport androidx.compose.run"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/util/AutoAdvanceLocal.kt",
"chars": 802,
"preview": "package com.thomaskioko.tvmaniac.compose.util\n\nimport androidx.compose.runtime.ProvidableCompositionLocal\nimport android"
},
{
"path": "android-designsystem/src/main/kotlin/com/thomaskioko/tvmaniac/compose/util/DynamicTheming.kt",
"chars": 5524,
"preview": "package com.thomaskioko.tvmaniac.compose.util\n\nimport android.content.Context\nimport androidx.collection.LruCache\nimport"
},
{
"path": "android-designsystem/src/main/res/values/strings.xml",
"chars": 131,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <string name=\"app_name\" translatable=\"false\">TvManiac</string>\n</"
},
{
"path": "android-designsystem/src/test/kotlin/com/thomaskioko/tvmaniac/compose/roborazzi/NotificationRationaleContentScreenshotTest.kt",
"chars": 1206,
"preview": "package com.thomaskioko.tvmaniac.compose.roborazzi\n\nimport androidx.activity.ComponentActivity\nimport androidx.compose.m"
},
{
"path": "android-designsystem/src/test/kotlin/com/thomaskioko/tvmaniac/compose/roborazzi/TvManiacSnackBarScreenshotTest.kt",
"chars": 2295,
"preview": "package com.thomaskioko.tvmaniac.compose.roborazzi\n\nimport androidx.activity.ComponentActivity\nimport androidx.compose.m"
},
{
"path": "api/tmdb/api/build.gradle.kts",
"chars": 294,
"preview": "plugins {\n alias(libs.plugins.app.kmp)\n}\n\nscaffold {\n useSerialization()\n}\n\nkotlin {\n sourceSets {\n comm"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/TmdbConfig.kt",
"chars": 105,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api\n\npublic interface TmdbConfig {\n public val apiKey: String\n}\n"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/TmdbSeasonDetailsNetworkDataSource.kt",
"chars": 488,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api\n\nimport com.thomaskioko.tvmaniac.core.networkutil.api.model.ApiResponse\nimport"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/TmdbShowDetailsNetworkDataSource.kt",
"chars": 1213,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api\n\nimport com.thomaskioko.tvmaniac.core.networkutil.api.model.ApiResponse\nimport"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/TmdbShowsNetworkDataSource.kt",
"chars": 3559,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api\n\nimport com.thomaskioko.tvmaniac.core.networkutil.api.model.ApiResponse\nimport"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/CreditsResponse.kt",
"chars": 555,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/EpisodesResponse.kt",
"chars": 747,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/GenreResponse.kt",
"chars": 258,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/ImagesResponse.kt",
"chars": 585,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/LastEpisodeToAirResponse.kt",
"chars": 804,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/NetworksResponse.kt",
"chars": 320,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/NextEpisodeToAirResponse.kt",
"chars": 804,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/SeasonsResponse.kt",
"chars": 607,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/TmdbGenreResult.kt",
"chars": 247,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/TmdbSeasonDetailsResponse.kt",
"chars": 789,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/TmdbShowDetailsResponse.kt",
"chars": 1629,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/TmdbShowResponse.kt",
"chars": 1226,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/VideosResponse.kt",
"chars": 706,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/api/model/WatchProvidersResult.kt",
"chars": 1503,
"preview": "package com.thomaskioko.tvmaniac.tmdb.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Se"
},
{
"path": "api/tmdb/implementation/build.gradle.kts",
"chars": 1186,
"preview": "plugins {\n alias(libs.plugins.app.kmp)\n}\n\nscaffold {\n addAndroidTarget()\n useSerialization()\n useMetro()\n}\n\n"
},
{
"path": "api/tmdb/implementation/src/androidMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformBindingContainer.kt",
"chars": 624,
"preview": "package com.thomaskioko.tvmaniac.tmdb.implementation\n\nimport com.thomaskioko.tvmaniac.core.base.TmdbApi\nimport dev.zacsw"
},
{
"path": "api/tmdb/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/DefaultTmdbSeasonDetailsNetworkDataSource.kt",
"chars": 1212,
"preview": "package com.thomaskioko.tvmaniac.tmdb.implementation\n\nimport com.thomaskioko.tvmaniac.core.base.TmdbApi\nimport com.thoma"
},
{
"path": "api/tmdb/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/DefaultTmdbShowDetailsNetworkDataSource.kt",
"chars": 2202,
"preview": "package com.thomaskioko.tvmaniac.tmdb.implementation\n\nimport com.thomaskioko.tvmaniac.core.base.TmdbApi\nimport com.thoma"
},
{
"path": "api/tmdb/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/DefaultTmdbShowsNetworkDataSource.kt",
"chars": 5130,
"preview": "package com.thomaskioko.tvmaniac.tmdb.implementation\n\nimport com.thomaskioko.tvmaniac.core.base.TmdbApi\nimport com.thoma"
},
{
"path": "api/tmdb/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbBindingContainer.kt",
"chars": 1511,
"preview": "package com.thomaskioko.tvmaniac.tmdb.implementation\n\nimport com.thomaskioko.tvmaniac.appconfig.ApplicationInfo\nimport c"
},
{
"path": "api/tmdb/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbClient.kt",
"chars": 2984,
"preview": "package com.thomaskioko.tvmaniac.tmdb.implementation\n\nimport com.thomaskioko.tvmaniac.core.connectivity.api.InternetConn"
},
{
"path": "api/tmdb/implementation/src/iosMain/kotlin/com/thomaskioko/tvmaniac/tmdb/implementation/TmdbPlatformBindingContainer.kt",
"chars": 624,
"preview": "package com.thomaskioko.tvmaniac.tmdb.implementation\n\nimport com.thomaskioko.tvmaniac.core.base.TmdbApi\nimport dev.zacsw"
},
{
"path": "api/trakt/api/build.gradle.kts",
"chars": 239,
"preview": "plugins {\n alias(libs.plugins.app.kmp)\n}\n\nscaffold {\n useSerialization()\n}\n\nkotlin {\n sourceSets {\n comm"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktCalendarRemoteDataSource.kt",
"chars": 375,
"preview": "package com.thomaskioko.tvmaniac.trakt.api\n\nimport com.thomaskioko.tvmaniac.core.networkutil.api.model.ApiResponse\nimpor"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktConfig.kt",
"chars": 180,
"preview": "package com.thomaskioko.tvmaniac.trakt.api\n\npublic interface TraktConfig {\n public val clientId: String\n public va"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktEpisodeHistoryRemoteDataSource.kt",
"chars": 665,
"preview": "package com.thomaskioko.tvmaniac.trakt.api\n\nimport com.thomaskioko.tvmaniac.core.networkutil.api.model.ApiResponse\nimpor"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktListRemoteDataSource.kt",
"chars": 1797,
"preview": "package com.thomaskioko.tvmaniac.trakt.api\n\nimport com.thomaskioko.tvmaniac.core.networkutil.api.model.ApiResponse\nimpor"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktShowsRemoteDataSource.kt",
"chars": 9543,
"preview": "package com.thomaskioko.tvmaniac.trakt.api\n\nimport com.thomaskioko.tvmaniac.core.networkutil.api.model.ApiResponse\nimpor"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktSyncRemoteDataSource.kt",
"chars": 325,
"preview": "package com.thomaskioko.tvmaniac.trakt.api\n\nimport com.thomaskioko.tvmaniac.core.networkutil.api.model.ApiResponse\nimpor"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktTokenRemoteDataSource.kt",
"chars": 587,
"preview": "package com.thomaskioko.tvmaniac.trakt.api\n\nimport com.thomaskioko.tvmaniac.core.networkutil.api.model.ApiResponse\nimpor"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/TraktUserRemoteDataSource.kt",
"chars": 631,
"preview": "package com.thomaskioko.tvmaniac.trakt.api\n\nimport com.thomaskioko.tvmaniac.core.networkutil.api.model.ApiResponse\nimpor"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/AccessTokenBody.kt",
"chars": 455,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/RefreshAccessTokenBody.kt",
"chars": 504,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktAccessRefreshTokenResponse.kt",
"chars": 521,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktAccessTokenResponse.kt",
"chars": 514,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktAddShowRequest.kt",
"chars": 484,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktAddShowToListResponse.kt",
"chars": 1072,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktCalendarResponse.kt",
"chars": 1005,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktCreateListRequest.kt",
"chars": 664,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktCreateListResponse.kt",
"chars": 506,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktFollowedShowResponse.kt",
"chars": 916,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktGenreResponse.kt",
"chars": 271,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktLastActivitiesResponse.kt",
"chars": 1309,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktNextEpisodeResponse.kt",
"chars": 665,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktPeopleResponse.kt",
"chars": 1714,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktPersonalListsResponse.kt",
"chars": 862,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktRemoveShowFromListResponse.kt",
"chars": 497,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktSeasonEpisodesResponse.kt",
"chars": 1478,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktSeasonsResponse.kt",
"chars": 756,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktShowsResponse.kt",
"chars": 2407,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktSyncModels.kt",
"chars": 2778,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktUserResponse.kt",
"chars": 650,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktUserStatsResponse.kt",
"chars": 1054,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktVideosResponse.kt",
"chars": 641,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/trakt/api/model/TraktWatchedProgressResponse.kt",
"chars": 580,
"preview": "package com.thomaskioko.tvmaniac.trakt.api.model\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.S"
},
{
"path": "api/trakt/implementation/build.gradle.kts",
"chars": 1868,
"preview": "plugins {\n alias(libs.plugins.app.kmp)\n}\n\nscaffold {\n addAndroidTarget()\n useMetro()\n useSerialization()\n}\n\n"
},
{
"path": "api/trakt/implementation/src/androidMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktPlatformBindingContainer.kt",
"chars": 628,
"preview": "package com.thomaskioko.trakt.service.implementation\n\nimport com.thomaskioko.tvmaniac.core.base.TraktApi\nimport dev.zacs"
},
{
"path": "api/trakt/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktAuthPlugin.kt",
"chars": 1878,
"preview": "package com.thomaskioko.trakt.service.implementation\n\nimport com.thomaskioko.tvmaniac.core.networkutil.api.extensions.Re"
},
{
"path": "api/trakt/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktBindingContainer.kt",
"chars": 1655,
"preview": "package com.thomaskioko.trakt.service.implementation\n\nimport com.thomaskioko.tvmaniac.appconfig.ApplicationInfo\nimport c"
},
{
"path": "api/trakt/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktHttpClient.kt",
"chars": 6313,
"preview": "package com.thomaskioko.trakt.service.implementation\n\nimport com.thomaskioko.tvmaniac.core.connectivity.api.InternetConn"
},
{
"path": "api/trakt/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/api/DefaultTraktCalendarRemoteDataSource.kt",
"chars": 1220,
"preview": "package com.thomaskioko.trakt.service.implementation.api\n\nimport com.thomaskioko.tvmaniac.core.base.TraktApi\nimport com."
},
{
"path": "api/trakt/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/api/DefaultTraktEpisodeRemoteDataSource.kt",
"chars": 2140,
"preview": "package com.thomaskioko.trakt.service.implementation.api\n\nimport com.thomaskioko.tvmaniac.core.base.TraktApi\nimport com."
},
{
"path": "api/trakt/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/api/DefaultTraktListRemoteDataSource.kt",
"chars": 6559,
"preview": "package com.thomaskioko.trakt.service.implementation.api\n\nimport com.thomaskioko.tvmaniac.core.base.TraktApi\nimport com."
},
{
"path": "api/trakt/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/api/DefaultTraktShowsRemoteDataSource.kt",
"chars": 7329,
"preview": "package com.thomaskioko.trakt.service.implementation.api\n\nimport com.thomaskioko.tvmaniac.core.base.TraktApi\nimport com."
},
{
"path": "api/trakt/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/api/DefaultTraktSyncRemoteDataSource.kt",
"chars": 1065,
"preview": "package com.thomaskioko.trakt.service.implementation.api\n\nimport com.thomaskioko.tvmaniac.core.base.TraktApi\nimport com."
},
{
"path": "api/trakt/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/api/DefaultTraktTokenRemoteDataSource.kt",
"chars": 3135,
"preview": "package com.thomaskioko.trakt.service.implementation.api\n\nimport com.thomaskioko.tvmaniac.core.base.TraktApi\nimport com."
},
{
"path": "api/trakt/implementation/src/commonMain/kotlin/com/thomaskioko/trakt/service/implementation/api/DefaultTraktUserRemoteDataSource.kt",
"chars": 1740,
"preview": "package com.thomaskioko.trakt.service.implementation.api\n\nimport com.thomaskioko.tvmaniac.core.base.TraktApi\nimport com."
},
{
"path": "api/trakt/implementation/src/commonTest/kotlin/com/thomaskioko/trakt/service/implementation/TraktAuthGuardPluginTest.kt",
"chars": 2556,
"preview": "package com.thomaskioko.trakt.service.implementation\n\nimport com.thomaskioko.tvmaniac.core.networkutil.api.extensions.Re"
},
{
"path": "api/trakt/implementation/src/iosMain/kotlin/com/thomaskioko/trakt/service/implementation/TraktPlatformBindingContainer.kt",
"chars": 628,
"preview": "package com.thomaskioko.trakt.service.implementation\n\nimport com.thomaskioko.tvmaniac.core.base.TraktApi\nimport dev.zacs"
},
{
"path": "api/trakt/implementation/src/jvmTest/kotlin/com/thomaskioko/trakt/service/implementation/TestResourceLoader.jvm.kt",
"chars": 187,
"preview": "package com.thomaskioko.trakt.service.implementation\n\ninternal fun loadJson(fileName: String): String =\n Thread.curre"
},
{
"path": "api/trakt/implementation/src/jvmTest/kotlin/com/thomaskioko/trakt/service/implementation/api/DefaultTraktListRemoteDataSourceTest.kt",
"chars": 7269,
"preview": "package com.thomaskioko.trakt.service.implementation.api\n\nimport com.thomaskioko.trakt.service.implementation.TraktAuthG"
},
{
"path": "api/trakt/implementation/src/jvmTest/resources/trakt_add_show_response.json",
"chars": 193,
"preview": "{\n \"added\": {\n \"shows\": 1\n },\n \"existing\": {\n \"shows\": 0\n },\n \"not_found\": {\n \"shows\": []\n },\n \"list\": {"
},
{
"path": "api/trakt/implementation/src/jvmTest/resources/trakt_error_response.json",
"chars": 77,
"preview": "{\n \"error\": \"unauthorized\",\n \"error_description\": \"invalid access token\"\n}\n"
},
{
"path": "api/trakt/implementation/src/jvmTest/resources/trakt_user_response.json",
"chars": 174,
"preview": "{\n \"username\": \"sean\",\n \"name\": \"Sean Rudford\",\n \"images\": {\n \"avatar\": {\n \"full\": \"https://example.com/avata"
},
{
"path": "app/benchmark-rules.pro",
"chars": 15,
"preview": "-dontobfuscate\n"
},
{
"path": "app/build.gradle.kts",
"chars": 11792,
"preview": "import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension\n\nplugins {\n alias(libs.plugins.app.appl"
},
{
"path": "app/lint-baseline.xml",
"chars": 82265,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<issues format=\"6\" by=\"lint 8.12.0\" type=\"baseline\" client=\"gradle\" dependencies="
},
{
"path": "app/proguard-rules.pro",
"chars": 3572,
"preview": "-verbose\n-allowaccessmodification\n-repackageclasses\n\n# AndroidX + support library contains references to newer platform "
},
{
"path": "app/src/androidTest/kotlin/com/thomaskioko/tvmaniac/app/test/runner/TvManiacInstrumentationRunner.kt",
"chars": 1219,
"preview": "package com.thomaskioko.tvmaniac.app.test.runner\n\nimport android.app.Application\nimport android.content.Context\nimport a"
},
{
"path": "app/src/debug/AndroidManifest.xml",
"chars": 1785,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:to"
},
{
"path": "app/src/debug/kotlin/com/thomaskioko/tvmaniac/app/debug/DebugNotificationIconProvider.kt",
"chars": 715,
"preview": "package com.thomaskioko.tvmaniac.app.debug\n\nimport com.thomaskioko.tvmaniac.app.R\nimport com.thomaskioko.tvmaniac.app.ut"
},
{
"path": "app/src/debug/kotlin/com/thomaskioko/tvmaniac/app/debug/DebugNotificationInitializer.kt",
"chars": 1219,
"preview": "package com.thomaskioko.tvmaniac.app.debug\n\nimport com.thomaskioko.tvmaniac.core.base.IoCoroutineScope\nimport com.thomas"
},
{
"path": "app/src/debug/kotlin/com/thomaskioko/tvmaniac/app/debug/di/DebugNotificationInitializerBindingContainer.kt",
"chars": 708,
"preview": "package com.thomaskioko.tvmaniac.app.debug.di\n\nimport com.thomaskioko.tvmaniac.app.debug.DebugNotificationInitializer\nim"
},
{
"path": "app/src/debug/res/drawable/ic_app_launcher.xml",
"chars": 29892,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"512dp\"\n android:height=\"512dp\"\n"
},
{
"path": "app/src/debug/res/drawable/ic_debug_bug.xml",
"chars": 711,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:widt"
},
{
"path": "app/src/debug/res/drawable/ic_launcher_foreground.xml",
"chars": 30026,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"108dp\"\n android:height=\"108dp\"\n"
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 2107,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:tool"
},
{
"path": "app/src/main/generated/baselineProfiles/baseline-prof.txt",
"chars": 2713177,
"preview": "Lamazon/lastmile/inject/ComThomaskiokoTvmaniacCoreBaseDiBaseAndroidComponent;\nLamazon/lastmile/inject/ComThomaskiokoTvma"
},
{
"path": "app/src/main/generated/baselineProfiles/startup-prof.txt",
"chars": 2713177,
"preview": "Lamazon/lastmile/inject/ComThomaskiokoTvmaniacCoreBaseDiBaseAndroidComponent;\nLamazon/lastmile/inject/ComThomaskiokoTvma"
},
{
"path": "app/src/main/kotlin/com/thomaskioko/tvmaniac/app/MainActivity.kt",
"chars": 5214,
"preview": "package com.thomaskioko.tvmaniac.app\n\nimport android.animation.ObjectAnimator\nimport android.content.Intent\nimport andro"
},
{
"path": "app/src/main/kotlin/com/thomaskioko/tvmaniac/app/TvManicApplication.kt",
"chars": 1810,
"preview": "package com.thomaskioko.tvmaniac.app\n\nimport android.app.Application\nimport android.os.Build\nimport android.os.StrictMod"
},
{
"path": "app/src/main/kotlin/com/thomaskioko/tvmaniac/app/di/ActivityGraph.kt",
"chars": 2282,
"preview": "package com.thomaskioko.tvmaniac.app.di\n\nimport androidx.activity.ComponentActivity\nimport com.arkivanov.decompose.Compo"
},
{
"path": "app/src/main/kotlin/com/thomaskioko/tvmaniac/app/di/ApplicationGraph.kt",
"chars": 727,
"preview": "package com.thomaskioko.tvmaniac.app.di\n\nimport android.app.Application\nimport com.thomaskioko.tvmaniac.app.util.TvMania"
},
{
"path": "app/src/main/kotlin/com/thomaskioko/tvmaniac/app/util/AppNotificationIconProvider.kt",
"chars": 489,
"preview": "package com.thomaskioko.tvmaniac.app.util\n\nimport com.thomaskioko.tvmaniac.app.R\nimport com.thomaskioko.tvmaniac.core.no"
},
{
"path": "app/src/main/kotlin/com/thomaskioko/tvmaniac/app/util/TvManiacWorkerFactory.kt",
"chars": 924,
"preview": "package com.thomaskioko.tvmaniac.app.util\n\nimport android.content.Context\nimport androidx.work.ListenableWorker\nimport a"
},
{
"path": "app/src/main/res/drawable/ic_app_launcher.xml",
"chars": 29892,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"512dp\"\n android:height=\"512dp\"\n"
},
{
"path": "app/src/main/res/drawable/ic_launcher_background.xml",
"chars": 4867,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector\n android:height=\"108dp\"\n android:width=\"108dp\"\n android:viewport"
},
{
"path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
"chars": 30026,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"108dp\"\n android:height=\"108dp\"\n"
},
{
"path": "app/src/main/res/drawable/ic_launcher_monochrome.xml",
"chars": 24904,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:widt"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 340,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 340,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 116,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"splash_background\">#F8FDFF</color>\n</resources>\n"
},
{
"path": "app/src/main/res/values/themes.xml",
"chars": 901,
"preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n <style name=\"NightAdjusted.Theme.TvManiac\" parent=\"android"
},
{
"path": "app/src/main/res/values-night/colors.xml",
"chars": 116,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"splash_background\">#373737</color>\n</resources>\n"
},
{
"path": "app/src/main/res/values-night/themes.xml",
"chars": 829,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n <style name=\"NightA"
},
{
"path": "app/src/main/res/xml/backup_rules.xml",
"chars": 172,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<full-backup-content>\n <exclude domain=\"sharedpref\" path=\".\" />\n <exclude d"
},
{
"path": "app/src/main/res/xml/data_extraction_rules.xml",
"chars": 328,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<data-extraction-rules>\n <cloud-backup>\n <exclude domain=\"sharedpref\" /"
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/BaseAppFlowTest.kt",
"chars": 6965,
"preview": "package com.thomaskioko.tvmaniac.app.test\n\nimport androidx.compose.ui.test.AndroidComposeUiTest\nimport androidx.compose."
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/TestAppComponent.kt",
"chars": 891,
"preview": "package com.thomaskioko.tvmaniac.app.test\n\nimport android.app.Application\nimport androidx.datastore.core.DataStore\nimpor"
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/TvManiacTestApplication.kt",
"chars": 1734,
"preview": "package com.thomaskioko.tvmaniac.app.test\n\nimport android.app.Application\nimport androidx.work.testing.WorkManagerTestIn"
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/compose/TvManiacTestActivity.kt",
"chars": 1595,
"preview": "package com.thomaskioko.tvmaniac.app.test.compose\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\ni"
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/compose/flows/calendar/CalendarFlowTest.kt",
"chars": 3566,
"preview": "package com.thomaskioko.tvmaniac.app.test.compose.flows.calendar\n\nimport com.thomaskioko.tvmaniac.app.test.BaseAppFlowTe"
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/compose/flows/discover/DiscoverToSeasonDetailsFlowTest.kt",
"chars": 1695,
"preview": "package com.thomaskioko.tvmaniac.app.test.compose.flows.discover\n\nimport com.thomaskioko.tvmaniac.app.test.BaseAppFlowTe"
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/compose/flows/discover/DiscoverToShowDetailsFollowFlowTest.kt",
"chars": 1137,
"preview": "package com.thomaskioko.tvmaniac.app.test.compose.flows.discover\n\nimport com.thomaskioko.tvmaniac.app.test.BaseAppFlowTe"
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/compose/flows/library/LibraryFlowTest.kt",
"chars": 2508,
"preview": "package com.thomaskioko.tvmaniac.app.test.compose.flows.library\n\nimport com.thomaskioko.tvmaniac.app.test.BaseAppFlowTes"
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/compose/flows/search/SearchFlowTest.kt",
"chars": 1562,
"preview": "package com.thomaskioko.tvmaniac.app.test.compose.flows.search\n\nimport com.thomaskioko.tvmaniac.app.test.BaseAppFlowTest"
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/compose/flows/seasons/SeasonFlowTest.kt",
"chars": 4088,
"preview": "package com.thomaskioko.tvmaniac.app.test.compose.flows.seasons\n\nimport com.thomaskioko.tvmaniac.app.test.BaseAppFlowTes"
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/compose/flows/settings/SettingsFlowTest.kt",
"chars": 1980,
"preview": "package com.thomaskioko.tvmaniac.app.test.compose.flows.settings\n\nimport com.thomaskioko.tvmaniac.app.test.BaseAppFlowTe"
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/compose/flows/sheet/EpisodeSheetFlowTest.kt",
"chars": 4120,
"preview": "package com.thomaskioko.tvmaniac.app.test.compose.flows.sheet\n\nimport com.thomaskioko.tvmaniac.app.test.AppFlowScope\nimp"
},
{
"path": "app/src/sharedTest/kotlin/com/thomaskioko/tvmaniac/app/test/compose/flows/showdetails/ShowDetailsFeaturesFlowTest.kt",
"chars": 2076,
"preview": "package com.thomaskioko.tvmaniac.app.test.compose.flows.showdetails\n\nimport com.thomaskioko.tvmaniac.app.test.BaseAppFlo"
}
]
// ... and 1516 more files (download for full content)
About this extraction
This page contains the full source code of the thomaskioko/tv-maniac GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 1716 files (9.6 MB), approximately 2.7M tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.