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/ │ │ │ │ │ │ ├── HorizontalShowContentView.swift │ │ │ │ │ │ └── ShowContentItemView.swift │ │ │ │ │ ├── SeasonChipViewList.swift │ │ │ │ │ ├── SeasonProgress/ │ │ │ │ │ │ ├── SeasonProgressCard.swift │ │ │ │ │ │ ├── SeasonProgressSection.swift │ │ │ │ │ │ └── SegmentedProgressBar.swift │ │ │ │ │ ├── SectionHeaderView.swift │ │ │ │ │ ├── SelectionChip.swift │ │ │ │ │ ├── ShowDetails/ │ │ │ │ │ │ ├── HeaderView.swift │ │ │ │ │ │ ├── ShowHeaderInfoView.swift │ │ │ │ │ │ └── ShowInfoView.swift │ │ │ │ │ ├── SnapCarousel.swift │ │ │ │ │ ├── StatsCardItem.swift │ │ │ │ │ ├── TextTitlePill.swift │ │ │ │ │ ├── ThemeSelector/ │ │ │ │ │ │ ├── ThemePreviewSwatch.swift │ │ │ │ │ │ └── ThemeSelectorView.swift │ │ │ │ │ ├── ThemedProgressView.swift │ │ │ │ │ ├── Toast/ │ │ │ │ │ │ ├── Toast.swift │ │ │ │ │ │ ├── ToastManager.swift │ │ │ │ │ │ ├── ToastModifier.swift │ │ │ │ │ │ └── ToastView.swift │ │ │ │ │ ├── TopBar.swift │ │ │ │ │ ├── TrailerListView.swift │ │ │ │ │ ├── TransparentBlurView.swift │ │ │ │ │ ├── Watchlist/ │ │ │ │ │ │ ├── TraktListSelectorContent.swift │ │ │ │ │ │ └── WatchListItemView.swift │ │ │ │ │ └── YoutubeItemView.swift │ │ │ │ ├── Core/ │ │ │ │ │ ├── CenteredFullScreenView.swift │ │ │ │ │ ├── ImageConfiguration.swift │ │ │ │ │ ├── KeyboardHeightManager.swift │ │ │ │ │ └── ViewConstants.swift │ │ │ │ ├── Effects/ │ │ │ │ │ └── ButtonElevationEffect.swift │ │ │ │ ├── Extensions/ │ │ │ │ │ ├── Geometry+Extensions.swift │ │ │ │ │ ├── HorizontalAlignment.swift │ │ │ │ │ ├── PrintExtension.swift │ │ │ │ │ └── View+onSizeChange.swift │ │ │ │ ├── Screens/ │ │ │ │ │ ├── Calendar/ │ │ │ │ │ │ └── CalendarScreen.swift │ │ │ │ │ ├── Discover/ │ │ │ │ │ │ ├── DiscoverListContent.swift │ │ │ │ │ │ └── DiscoverScreen.swift │ │ │ │ │ ├── Library/ │ │ │ │ │ │ ├── LibraryScreen.swift │ │ │ │ │ │ └── MoreShowsScreen.swift │ │ │ │ │ ├── Profile/ │ │ │ │ │ │ ├── DebugScreen.swift │ │ │ │ │ │ ├── ProfileScreen.swift │ │ │ │ │ │ └── SettingsScreen.swift │ │ │ │ │ ├── Progress/ │ │ │ │ │ │ └── ProgressScreen.swift │ │ │ │ │ ├── Search/ │ │ │ │ │ │ ├── SearchScreen.swift │ │ │ │ │ │ └── SearchScreenPreviews.swift │ │ │ │ │ ├── ShowDetails/ │ │ │ │ │ │ ├── SeasonDetailsScreen.swift │ │ │ │ │ │ └── ShowDetailsScreen.swift │ │ │ │ │ └── Watchlist/ │ │ │ │ │ ├── WatchlistGridItem.swift │ │ │ │ │ └── WatchlistScreen.swift │ │ │ │ ├── Styles/ │ │ │ │ │ ├── RoundedProgressIndicatorStyle.swift │ │ │ │ │ └── TransparentGroupBox.swift │ │ │ │ ├── Theme/ │ │ │ │ │ ├── Colors/ │ │ │ │ │ │ ├── AmberColorScheme.swift │ │ │ │ │ │ ├── AquaColorScheme.swift │ │ │ │ │ │ ├── AutumnColorScheme.swift │ │ │ │ │ │ ├── ColorScheme.swift │ │ │ │ │ │ ├── ColorTokens.swift │ │ │ │ │ │ ├── CrimsonColorScheme.swift │ │ │ │ │ │ ├── DarkColorScheme.swift │ │ │ │ │ │ ├── LightColorScheme.swift │ │ │ │ │ │ ├── SnowColorScheme.swift │ │ │ │ │ │ └── TerminalColorScheme.swift │ │ │ │ │ ├── Environment/ │ │ │ │ │ │ ├── ThemeEnvironment.swift │ │ │ │ │ │ └── TvManiacTheme.swift │ │ │ │ │ ├── Preview/ │ │ │ │ │ │ └── ThemedPreview.swift │ │ │ │ │ ├── Shape/ │ │ │ │ │ │ └── ShapeTokens.swift │ │ │ │ │ ├── Spacing/ │ │ │ │ │ │ └── SpacingTokens.swift │ │ │ │ │ └── Typography/ │ │ │ │ │ └── TypographyScheme.swift │ │ │ │ ├── Utilities/ │ │ │ │ │ └── BindingFactories.swift │ │ │ │ └── ViewModifiers/ │ │ │ │ ├── SwipeBackGesture.swift │ │ │ │ └── TestTagModifier.swift │ │ │ └── Tests/ │ │ │ └── SwiftUIComponentsTests/ │ │ │ ├── BorderTextViewTest.swift │ │ │ ├── CalendarScreenTest.swift │ │ │ ├── CastCardViewTest.swift │ │ │ ├── CastListViewTest.swift │ │ │ ├── ChevronTitleTest.swift │ │ │ ├── ChipViewTest.swift │ │ │ ├── CircularButtonTest.swift │ │ │ ├── DebugScreenTest.swift │ │ │ ├── DiscoverScreenTest.swift │ │ │ ├── EpisodeCollapsibleTest.swift │ │ │ ├── EpisodeDetailSheetContentTest.swift │ │ │ ├── EpisodeItemViewTest.swift │ │ │ ├── EpisodeListViewTest.swift │ │ │ ├── FilledImageButtonTest.swift │ │ │ ├── FullScreenViewTest.swift │ │ │ ├── GridViewTest.swift │ │ │ ├── HeaderViewTest.swift │ │ │ ├── HorizontalItemListViewTest.swift │ │ │ ├── HorizontalShowContentViewTest.swift │ │ │ ├── LibraryScreenTest.swift │ │ │ ├── MoreShowsScreenTest.swift │ │ │ ├── NavigationTopBarTest.swift │ │ │ ├── NextEpisodesSectionTest.swift │ │ │ ├── NotificationRationaleSheetTest.swift │ │ │ ├── OutlinedButtonTest.swift │ │ │ ├── OverviewBoxViewTest.swift │ │ │ ├── PosterItemViewTest.swift │ │ │ ├── ProfileScreenTest.swift │ │ │ ├── ProgressScreenTest.swift │ │ │ ├── ProviderItemViewTest.swift │ │ │ ├── ProviderListViewTest.swift │ │ │ ├── SearchItemViewTest.swift │ │ │ ├── SearchScreenTest.swift │ │ │ ├── SeasonChipViewListTest.swift │ │ │ ├── SeasonDetailsScreenTest.swift │ │ │ ├── SettingsScreenTest.swift │ │ │ ├── ShowContentItemViewTest.swift │ │ │ ├── ShowDetailsScreenTest.swift │ │ │ ├── ShowInfoViewTest.swift │ │ │ ├── SnapshotTestCase.swift │ │ │ ├── ThemeSelectorViewTest.swift │ │ │ ├── ToastViewTest.swift │ │ │ ├── TopBarTest.swift │ │ │ ├── TrailerListViewTest.swift │ │ │ ├── TraktListSelectorContentTest.swift │ │ │ ├── WatchlistScreenTest.swift │ │ │ └── YoutubeItemViewTest.swift │ │ ├── TraktAuthKit/ │ │ │ ├── .gitignore │ │ │ ├── Package.swift │ │ │ └── Sources/ │ │ │ └── TraktAuthKit/ │ │ │ ├── AppAuthCoordinator.swift │ │ │ ├── TraktAuthConfiguration.swift │ │ │ ├── TraktAuthError.swift │ │ │ ├── TraktCredential.swift │ │ │ └── TraktOAuthClient.swift │ │ └── TvManiacKit/ │ │ ├── .gitignore │ │ ├── Package.swift │ │ └── Sources/ │ │ └── TvManiacKit/ │ │ ├── AppDelegate.swift │ │ ├── Architecture/ │ │ │ ├── ComponentHolder.swift │ │ │ ├── ObservableValue.swift │ │ │ └── StateValue.swift │ │ ├── AuthCoordinatorFactory.swift │ │ ├── Decompose/ │ │ │ └── DecomposeNavigationStack.swift │ │ ├── Extensions/ │ │ │ ├── Mapper+Extensions.swift │ │ │ ├── Moko+Extensions.swift │ │ │ └── View+Extensions.swift │ │ ├── KmpLoggerBridge.swift │ │ ├── Models/ │ │ │ └── SwiftImageQuality.swift │ │ ├── Modifiers/ │ │ │ ├── AppThemeModifier.swift │ │ │ └── DebugTapGesture.swift │ │ ├── Navigation/ │ │ │ ├── ScreenRegistry+Typed.swift │ │ │ └── ScreenRegistry.swift │ │ ├── NotificationDelegate.swift │ │ ├── SFSafariViewWrapper.swift │ │ ├── SettingsAppStorage.swift │ │ ├── Theme/ │ │ │ └── TvManiacTypographyScheme+Moko.swift │ │ ├── ThemeUtilities.swift │ │ ├── TraktAuthCoordinator.swift │ │ └── TvManiacKit.swift │ ├── ios/ │ │ ├── App/ │ │ │ ├── AppIcon.swift │ │ │ └── iOSApp.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIconDebug.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Colors/ │ │ │ │ ├── Background.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── TabBackgroundColor.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── TextColor.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── accent.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── accentBlue.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── content_background.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── gradient_background.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── grey_200.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── grey_500.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── grey_900.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── text_color_bg.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── yellow_300.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── yellow_500.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── TvManiacIcon.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── TvManiacIconDebug.imageset/ │ │ │ │ └── Contents.json │ │ │ └── trakt_logo.imageset/ │ │ │ └── Contents.json │ │ ├── Components/ │ │ │ └── ColorScheme.swift │ │ ├── Feature/ │ │ │ └── Grid/ │ │ │ └── ShowGridView.swift │ │ ├── Info.plist │ │ ├── PrivacyInfo.xcprivacy │ │ └── UI/ │ │ ├── Debug/ │ │ │ └── DebugMenuView.swift │ │ ├── EpisodeDetail/ │ │ │ └── EpisodeDetailSheetView.swift │ │ ├── MoreShows/ │ │ │ └── MoreShowsView.swift │ │ ├── Root/ │ │ │ ├── RootNavigationView.swift │ │ │ └── ScreenRegistryBootstrap.swift │ │ ├── SeasonDetails/ │ │ │ └── SeasonDetailsView.swift │ │ ├── Settings/ │ │ │ └── SettingsView.swift │ │ ├── ShowDetails/ │ │ │ └── ShowDetailsView.swift │ │ ├── SplashScreen/ │ │ │ └── SplashView.swift │ │ ├── Tabs/ │ │ │ ├── Components/ │ │ │ │ ├── SortOptionsSheet.swift │ │ │ │ ├── TabContentView.swift │ │ │ │ ├── UpNextPageContent.swift │ │ │ │ └── WatchlistListItem.swift │ │ │ ├── DiscoverTab.swift │ │ │ ├── LibraryTab.swift │ │ │ ├── ProfileTab.swift │ │ │ ├── ProgressTab.swift │ │ │ ├── SearchTab.swift │ │ │ ├── TabBarView.swift │ │ │ └── WatchlistTab.swift │ │ └── Watchlist/ │ │ └── WatchlistSelector.swift │ └── tv-maniac.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ └── xcschemes/ │ ├── SwiftUIComponents.xcscheme │ ├── TvManiacKit.xcscheme │ └── tv-maniac.xcscheme ├── ios-framework/ │ ├── build.gradle.kts │ └── src/ │ └── iosMain/ │ └── kotlin/ │ └── com.thomaskioko.tvmaniac.iosframework/ │ ├── IosApplicationGraph.kt │ └── IosViewPresenterGraph.kt ├── navigation/ │ ├── api/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── navigation/ │ │ ├── NavDestination.kt │ │ ├── NavRoute.kt │ │ ├── NavRouteBinding.kt │ │ ├── NavRouteSerializer.kt │ │ ├── NavigationResultRegistry.kt │ │ ├── NavigationResultRequest.kt │ │ ├── NavigationResults.kt │ │ ├── Navigator.kt │ │ ├── RootChild.kt │ │ ├── ScreenDestination.kt │ │ ├── SheetChild.kt │ │ ├── SheetChildFactory.kt │ │ ├── SheetConfig.kt │ │ ├── SheetConfigBinding.kt │ │ ├── SheetConfigSerializer.kt │ │ ├── SheetDestination.kt │ │ └── SheetNavigator.kt │ ├── implementation/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── navigation/ │ │ │ ├── DefaultNavRouteSerializer.kt │ │ │ ├── DefaultNavigationResultRegistry.kt │ │ │ ├── DefaultNavigator.kt │ │ │ ├── DefaultSheetConfigSerializer.kt │ │ │ └── di/ │ │ │ └── NavigationMultibindings.kt │ │ └── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── navigation/ │ │ ├── DefaultNavRouteSerializerTest.kt │ │ ├── DefaultNavigationResultRegistryTest.kt │ │ ├── DefaultNavigatorTest.kt │ │ ├── DefaultSheetConfigSerializerTest.kt │ │ └── FakeNavigator.kt │ ├── testing/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── thomaskioko/ │ │ │ └── tvmaniac/ │ │ │ └── navigation/ │ │ │ └── testing/ │ │ │ ├── FakeSheetNavigator.kt │ │ │ ├── NavEvent.kt │ │ │ ├── NavigatorTurbine.kt │ │ │ └── TestNavigator.kt │ │ └── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── thomaskioko/ │ │ └── tvmaniac/ │ │ └── navigation/ │ │ └── testing/ │ │ └── NavigatorTest.kt │ └── ui/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ └── com/ │ └── thomaskioko/ │ └── tvmaniac/ │ └── navigation/ │ └── ui/ │ ├── ScreenContent.kt │ ├── SheetContent.kt │ └── di/ │ └── NavigationUiMultibindings.kt ├── release/ │ ├── RELEASE.md │ ├── app-release.aes │ ├── firebase-sa.aes │ └── play-service-account.aes ├── scripts/ │ ├── git-hooks/ │ │ └── pre-commit │ └── install-git-hooks.sh ├── settings.gradle.kts └── version.txt ================================================ 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
/g') echo "| [$fileName](https://github.com/${{ github.repository }}/blob/$BRANCH_NAME/$file) | ![](https://github.com/${{ github.repository }}/blob/$BRANCH_NAME/$file?raw=true) |" >> "$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<> "$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 ================================================ ================================================ FILE: .idea/codeStyles/codeStyleConfig.xml ================================================ ================================================ FILE: .idea/dictionaries/project.xml ================================================ tmdb trakt ================================================ 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` (for Decompose) or `StateFlow`. - 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 ================================================

# TvManiac ![Check](https://github.com/thomaskioko/tv-maniac/actions/workflows/ci.yml/badge.svg) ![kmp](https://img.shields.io/badge/multiplatform-%237F52FF.svg?style=for-the-badge&logo=kotlin&logoColor=white) ![compose](https://img.shields.io/badge/jetpack_compose-2bab6b.svg?style=for-the-badge&logo=android&logoColor=white) ![swiftui](https://img.shields.io/badge/swiftui-%23000000.svg?style=for-the-badge&logo=swift&logoColor=white) [![Download APK](https://img.shields.io/github/v/release/thomaskioko/tv-maniac?label=Download%20APK&logo=android&style=for-the-badge)](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 | |---|---| |