├── .github
└── workflows
│ ├── androidTests.yml
│ ├── build.yml
│ ├── refreshVersions.yml
│ ├── release-desktop.yml
│ └── release.yml
├── .gitignore
├── .idea
└── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── RELEASING.md
├── androidApp
├── .gitignore
├── build.gradle.kts
├── google-services.json
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── kotlin
│ └── com
│ │ └── mr3y
│ │ └── ludi
│ │ ├── LudiApplication.kt
│ │ ├── MainActivity.kt
│ │ └── ui
│ │ └── preview
│ │ └── LudiPreview.kt
│ ├── play
│ └── release-notes
│ │ └── en-US
│ │ └── beta.txt
│ └── res
│ ├── drawable-night
│ └── ic_splash.xml
│ ├── drawable
│ └── ic_splash.xml
│ ├── mipmap-anydpi-v26
│ └── ic_launcher.xml
│ ├── mipmap-anydpi-v33
│ └── ic_launcher.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_monochrome.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_monochrome.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_monochrome.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_monochrome.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_monochrome.png
│ ├── values-night-v27
│ └── themes.xml
│ ├── values-night
│ └── themes.xml
│ ├── values-v27
│ └── themes.xml
│ ├── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ └── data_extraction_rules.xml
├── build.gradle.kts
├── changelog_config.json
├── convention-plugins
├── gradle.properties
├── plugins
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ └── com
│ │ └── mr3y
│ │ └── ludi
│ │ └── gradle
│ │ ├── AndroidConventionPlugin.kt
│ │ └── CommonConventionPlugin.kt
└── settings.gradle.kts
├── desktopApp
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── jvmMain
│ ├── kotlin
│ └── com
│ │ └── mr3y
│ │ └── ludi
│ │ └── desktop
│ │ └── Main.kt
│ └── resources
│ ├── icon_light.ico
│ ├── icon_light.png
│ └── icon_light.xml
├── docs
├── PrivacyPolicy.md
└── README.md
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── index.html
├── lint.xml
├── settings.gradle.kts
├── shared
├── .gitignore
├── build.gradle.kts
└── src
│ ├── androidInstrumentedTest
│ └── kotlin
│ │ └── com
│ │ └── mr3y
│ │ └── ludi
│ │ └── shared
│ │ └── ui
│ │ ├── datastore
│ │ └── internal
│ │ │ └── DefaultProtoDataStoreMutatorTest.kt
│ │ └── presenter
│ │ ├── FakeGamesRepository.kt
│ │ ├── FakeProtoDataStoreMutator.kt
│ │ ├── MainDispatcherRule.kt
│ │ ├── OnBoardingViewModelTest.kt
│ │ └── SettingsViewModelTest.kt
│ ├── androidMain
│ ├── AndroidManifest.xml
│ └── kotlin
│ │ └── com
│ │ └── mr3y
│ │ └── ludi
│ │ └── shared
│ │ ├── core
│ │ ├── CrashlyticsReporting.kt
│ │ └── database
│ │ │ └── DatabaseFactory.android.kt
│ │ ├── di
│ │ ├── AndroidApplicationComponent.kt
│ │ ├── AndroidCrashReportingComponent.kt
│ │ ├── AndroidSqlDriverComponent.kt
│ │ ├── DatabaseDispatcherComponent.android.kt
│ │ ├── DealsFeatureComponent.kt
│ │ ├── DiscoverFeatureComponent.kt
│ │ ├── HostActivityComponent.kt
│ │ ├── HostActivityComponentOwner.kt
│ │ ├── NewsFeatureComponent.kt
│ │ ├── OnboardingFeatureComponent.kt
│ │ ├── SettingsFeatureComponent.kt
│ │ ├── TimeSourceComponent.android.kt
│ │ └── VoyagerIntegration.android.kt
│ │ └── ui
│ │ ├── components
│ │ ├── LudiAsyncImage.android.kt
│ │ ├── LudiChromeCustomTab.kt
│ │ └── LudiNoInternet.android.kt
│ │ ├── lifecycle
│ │ ├── ConnectionState.kt
│ │ └── ConnectivityStatus.kt
│ │ ├── presenter
│ │ └── FrameClock.android.kt
│ │ ├── resources
│ │ └── PlatformCompositionProvider.android.kt
│ │ ├── screens
│ │ ├── deals
│ │ │ └── DealsScreen.android.kt
│ │ ├── discover
│ │ │ └── DiscoverScreen.android.kt
│ │ ├── news
│ │ │ └── NewsScreen.android.kt
│ │ └── settings
│ │ │ └── SettingsScreen.android.kt
│ │ └── theme
│ │ └── ColorScheme.android.kt
│ ├── androidUnitTest
│ └── kotlin
│ │ └── com
│ │ └── mr3y
│ │ └── ludi
│ │ └── shared
│ │ └── ui
│ │ └── screens
│ │ ├── BaseRobolectricTest.kt
│ │ ├── Utils.kt
│ │ ├── deals
│ │ ├── DealsScreenTest.kt
│ │ └── FakeDealsState.kt
│ │ └── onboarding
│ │ ├── FakeOnboardingState.kt
│ │ ├── OnboardingScreenFABTest.kt
│ │ ├── Utils.kt
│ │ └── accessibility
│ │ └── OnboardingScreenAccessibilityTest.kt
│ ├── commonMain
│ ├── composeResources
│ │ └── drawable
│ │ │ ├── android.xml
│ │ │ ├── brutalgamer_logo.xml
│ │ │ ├── eurogamer_logo.xml
│ │ │ ├── game_spot_logo.xml
│ │ │ ├── gamerant_logo.xml
│ │ │ ├── giant_bomb_logo.xml
│ │ │ ├── gloriousgaming_logo.xml
│ │ │ ├── ign_logo.xml
│ │ │ ├── ios.xml
│ │ │ ├── linux.xml
│ │ │ ├── mmobomb_logo.xml
│ │ │ ├── nintendo_switch.xml
│ │ │ ├── pc.xml
│ │ │ ├── pcgamer_logo.xml
│ │ │ ├── pcgamesn_logo.xml
│ │ │ ├── pcinvasion_logo.xml
│ │ │ ├── placeholder.xml
│ │ │ ├── playstation.xml
│ │ │ ├── polygon_logo.xml
│ │ │ ├── rockpapershotgun_logo.xml
│ │ │ ├── tech_radar_logo.xml
│ │ │ ├── thegamer_logo.xml
│ │ │ ├── venturebeat_logo.xml
│ │ │ ├── vg247_logo.xml
│ │ │ └── xbox.xml
│ ├── kotlin
│ │ └── com
│ │ │ └── mr3y
│ │ │ └── ludi
│ │ │ └── shared
│ │ │ ├── App.kt
│ │ │ ├── LudiSharedState.kt
│ │ │ ├── core
│ │ │ ├── CrashReporting.kt
│ │ │ ├── Logger.kt
│ │ │ ├── database
│ │ │ │ ├── Adapters.kt
│ │ │ │ ├── DatabaseFactory.kt
│ │ │ │ ├── Mappers.kt
│ │ │ │ ├── Wrapper.kt
│ │ │ │ └── dao
│ │ │ │ │ └── ArticleEntitiesDao.kt
│ │ │ ├── internal
│ │ │ │ └── KermitLogger.kt
│ │ │ ├── model
│ │ │ │ ├── Article.kt
│ │ │ │ ├── Deal.kt
│ │ │ │ ├── Game.kt
│ │ │ │ ├── GiveawayEntry.kt
│ │ │ │ ├── MarkupText.kt
│ │ │ │ ├── NewReleaseArticle.kt
│ │ │ │ ├── NewsArticle.kt
│ │ │ │ ├── Result.kt
│ │ │ │ ├── ReviewArticle.kt
│ │ │ │ └── Source.kt
│ │ │ ├── network
│ │ │ │ ├── datasources
│ │ │ │ │ ├── RSSFeedDataSource.kt
│ │ │ │ │ └── internal
│ │ │ │ │ │ ├── CheapSharkDataSource.kt
│ │ │ │ │ │ ├── DefaultRSSFeedDataSource.kt
│ │ │ │ │ │ ├── GamerPowerDataSource.kt
│ │ │ │ │ │ └── RAWGDataSource.kt
│ │ │ │ ├── model
│ │ │ │ │ ├── ApiResult.kt
│ │ │ │ │ ├── CheapSharkDeal.kt
│ │ │ │ │ ├── GamerPowerGiveaway.kt
│ │ │ │ │ ├── RAWGPage.kt
│ │ │ │ │ └── TimeConversionsUtils.kt
│ │ │ │ ├── rssparser
│ │ │ │ │ ├── Parser.kt
│ │ │ │ │ └── internal
│ │ │ │ │ │ └── DefaultParser.kt
│ │ │ │ └── serialization
│ │ │ │ │ ├── CheapSharkModelsSerializers.kt
│ │ │ │ │ ├── GamerPowerModelsSerializers.kt
│ │ │ │ │ └── RAWGModelsSerializers.kt
│ │ │ ├── paging
│ │ │ │ ├── DealsPagingSource.kt
│ │ │ │ └── RAWGGamesPagingSource.kt
│ │ │ └── repository
│ │ │ │ ├── DealsRepository.kt
│ │ │ │ ├── GamesRepository.kt
│ │ │ │ ├── NewsRepository.kt
│ │ │ │ ├── internal
│ │ │ │ ├── DefaultDealsRepository.kt
│ │ │ │ ├── DefaultGamesRepository.kt
│ │ │ │ └── DefaultNewsRepository.kt
│ │ │ │ └── query
│ │ │ │ ├── DealsQueryParameters.kt
│ │ │ │ ├── GamesQueryParameters.kt
│ │ │ │ └── GiveawaysQueryParameters.kt
│ │ │ ├── di
│ │ │ ├── CoroutinesComponent.kt
│ │ │ ├── DataStoreComponent.kt
│ │ │ ├── DatabaseDispatcherComponent.kt
│ │ │ ├── LoggingComponent.kt
│ │ │ ├── NetworkComponent.kt
│ │ │ ├── RESTfulDataSourcesComponent.kt
│ │ │ ├── RSSFeedDataSourcesComponent.kt
│ │ │ ├── RepositoriesComponent.kt
│ │ │ ├── SharedApplicationComponent.kt
│ │ │ ├── SharedDatabaseComponent.kt
│ │ │ ├── SharedDealsFeatureComponent.kt
│ │ │ ├── SharedDiscoverFeatureComponent.kt
│ │ │ ├── SharedNewsFeatureComponent.kt
│ │ │ ├── SharedOnboardingFeatureComponent.kt
│ │ │ ├── SharedSettingsFeatureComponent.kt
│ │ │ ├── TimeSourceComponent.kt
│ │ │ ├── VoyagerIntegration.kt
│ │ │ └── annotations
│ │ │ │ ├── DealsFeatureScope.kt
│ │ │ │ ├── DiscoverFeatureScope.kt
│ │ │ │ ├── NewsFeatureScope.kt
│ │ │ │ ├── OnboardingFeatureScope.kt
│ │ │ │ ├── SettingsFeatureScope.kt
│ │ │ │ └── Singleton.kt
│ │ │ └── ui
│ │ │ ├── adaptive
│ │ │ └── LocalWindowSizeClass.kt
│ │ │ ├── components
│ │ │ ├── LudiAsyncImage.kt
│ │ │ ├── LudiChips.kt
│ │ │ ├── LudiErrorBox.kt
│ │ │ ├── LudiHeaders.kt
│ │ │ ├── LudiNavRail.kt
│ │ │ ├── LudiNoInternet.kt
│ │ │ ├── LudiParallaxAlignment.kt
│ │ │ ├── LudiRefreshIconButton.kt
│ │ │ └── placeholder
│ │ │ │ ├── LudiPlaceholderModifier.kt
│ │ │ │ ├── Placeholder.kt
│ │ │ │ ├── PlaceholderDefaults.kt
│ │ │ │ └── PlaceholderHighlight.kt
│ │ │ ├── datastore
│ │ │ ├── FavouriteGamesSerializer.kt
│ │ │ ├── FavouriteGenresSerializer.kt
│ │ │ ├── FollowedNewsDataSourceSerializer.kt
│ │ │ ├── PreferencesKeys.kt
│ │ │ ├── ProtoDataStoreMutator.kt
│ │ │ ├── Wrappers.kt
│ │ │ └── internal
│ │ │ │ └── DefaultProtoDataStoreMutator.kt
│ │ │ ├── navigation
│ │ │ ├── BottomBarTab.kt
│ │ │ └── PreferencesType.kt
│ │ │ ├── presenter
│ │ │ ├── DealsViewModel.kt
│ │ │ ├── DiscoverPagingFactory.kt
│ │ │ ├── DiscoverViewModel.kt
│ │ │ ├── EditPreferencesViewModel.kt
│ │ │ ├── NewsFeedThrottler.kt
│ │ │ ├── NewsViewModel.kt
│ │ │ ├── OnBoardingViewModel.kt
│ │ │ ├── SettingsViewModel.kt
│ │ │ └── model
│ │ │ │ ├── DealsState.kt
│ │ │ │ ├── DiscoverState.kt
│ │ │ │ ├── EditPreferencesState.kt
│ │ │ │ ├── NewsState.kt
│ │ │ │ ├── OnboardingState.kt
│ │ │ │ ├── SettingsState.kt
│ │ │ │ └── SupportedNewsDataSources.kt
│ │ │ ├── resources
│ │ │ ├── EnStrings.kt
│ │ │ ├── LudiStrings.kt
│ │ │ ├── PlatformCompositionProvider.kt
│ │ │ └── format.kt
│ │ │ ├── screens
│ │ │ ├── deals
│ │ │ │ ├── DealAndGiveawayCards.kt
│ │ │ │ ├── DealsFilters.kt
│ │ │ │ ├── DealsScreen.kt
│ │ │ │ ├── DealsTopAppBar.kt
│ │ │ │ └── SegmentedTabRow.kt
│ │ │ ├── discover
│ │ │ │ ├── DiscoverFilters.kt
│ │ │ │ ├── DiscoverScreen.kt
│ │ │ │ ├── DiscoverTopAppBar.kt
│ │ │ │ └── GameCards.kt
│ │ │ ├── home
│ │ │ │ └── HomeScreen.kt
│ │ │ ├── news
│ │ │ │ ├── ArticleTile.kt
│ │ │ │ ├── NewReleaseTile.kt
│ │ │ │ └── NewsScreen.kt
│ │ │ ├── onboarding
│ │ │ │ ├── DataSourcesPage.kt
│ │ │ │ ├── GamesPage.kt
│ │ │ │ ├── GenresPage.kt
│ │ │ │ └── OnboardingScreen.kt
│ │ │ └── settings
│ │ │ │ ├── EditPreferencesScreen.kt
│ │ │ │ └── SettingsScreen.kt
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── ColorScheme.kt
│ │ │ ├── Shape.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ ├── proto
│ │ ├── favourite_game.proto
│ │ ├── favourite_games.proto
│ │ ├── favourite_gaming_genre.proto
│ │ ├── favourite_gaming_genres.proto
│ │ ├── news_data_source.proto
│ │ └── news_data_sources.proto
│ └── sqldelight
│ │ ├── com
│ │ └── mr3y
│ │ │ └── ludi
│ │ │ └── shared
│ │ │ ├── Article.sq
│ │ │ └── ArticleSearchFTS.sq
│ │ ├── databases
│ │ ├── 1.db
│ │ └── 2.db
│ │ └── migrations
│ │ └── 1.sqm
│ ├── commonTest
│ └── kotlin
│ │ └── com
│ │ └── mr3y
│ │ └── ludi
│ │ └── shared
│ │ ├── MainDispatcherRule.kt
│ │ ├── core
│ │ ├── FakeCrashReporting.kt
│ │ ├── network
│ │ │ ├── datasources
│ │ │ │ └── internal
│ │ │ │ │ ├── CheapSharkDataSourceTest.kt
│ │ │ │ │ ├── DefaultRSSFeedDataSourceTest.kt
│ │ │ │ │ ├── GamerPowerDataSourceTest.kt
│ │ │ │ │ ├── RAWGDataSourceTest.kt
│ │ │ │ │ ├── RAWGMockResponses.kt
│ │ │ │ │ └── RSSFeedSamples.kt
│ │ │ ├── fixtures
│ │ │ │ ├── FakeRSSFeedDataSource.kt
│ │ │ │ ├── KtorClientForTesting.kt
│ │ │ │ └── TestLogger.kt
│ │ │ ├── model
│ │ │ │ └── ConvertersTest.kt
│ │ │ └── rssparser
│ │ │ │ └── FakeRSSParser.kt
│ │ └── repository
│ │ │ └── fixtures
│ │ │ └── FakeGamesRepository.kt
│ │ └── ui
│ │ └── presenter
│ │ ├── DiscoverViewModelMockData.kt
│ │ ├── DiscoverViewModelTest.kt
│ │ └── FakeDiscoverPagingFactory.kt
│ ├── desktopAndroidMain
│ └── kotlin
│ │ └── com
│ │ └── mr3y
│ │ └── ludi
│ │ └── shared
│ │ └── ui
│ │ └── resources
│ │ └── format.shared.kt
│ ├── desktopMain
│ └── kotlin
│ │ └── com
│ │ └── mr3y
│ │ └── ludi
│ │ └── shared
│ │ ├── AppCacheDir.kt
│ │ ├── AppDir.kt
│ │ ├── core
│ │ ├── BugsnagReporting.kt
│ │ └── database
│ │ │ └── DatabaseFactory.desktop.kt
│ │ ├── di
│ │ ├── DatabaseDispatcherComponent.desktop.kt
│ │ ├── DealsFeatureComponent.kt
│ │ ├── DesktopApplicationComponent.kt
│ │ ├── DesktopCrashReportingComponent.kt
│ │ ├── DesktopSqlDriverComponent.kt
│ │ ├── DiscoverFeatureComponent.kt
│ │ ├── HostWindowComponent.kt
│ │ ├── NewsFeatureComponent.kt
│ │ ├── OnboardingFeatureComponent.kt
│ │ ├── SettingsFeatureComponent.kt
│ │ ├── TimeSourceComponent.desktop.kt
│ │ └── VoyagerIntegration.desktop.kt
│ │ └── ui
│ │ ├── components
│ │ ├── LudiAsyncImage.desktop.kt
│ │ ├── LudiNoInternet.desktop.kt
│ │ └── OpenUrls.kt
│ │ ├── presenter
│ │ └── FrameClock.desktop.kt
│ │ ├── resources
│ │ └── PlatformCompositionProvider.desktop.kt
│ │ ├── screens
│ │ ├── deals
│ │ │ └── DealsScreen.desktop.kt
│ │ ├── discover
│ │ │ └── DiscoverScreen.desktop.kt
│ │ ├── news
│ │ │ └── NewsScreen.desktop.kt
│ │ └── settings
│ │ │ └── SettingsScreen.desktop.kt
│ │ └── theme
│ │ └── ColorScheme.desktop.kt
│ └── desktopTest
│ └── kotlin
│ └── com
│ └── mr3y
│ └── ludi
│ └── shared
│ └── core
│ └── database
│ └── dao
│ └── ArticleEntitiesDaoTest.kt
└── versions.properties
/.github/workflows/androidTests.yml:
--------------------------------------------------------------------------------
1 | name: Run android instrumented tests
2 |
3 | # Run Android instrumented tests once every day at 1am UTC.
4 | on:
5 | schedule:
6 | - cron: '0 1 * * *'
7 | workflow_dispatch:
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
11 | cancel-in-progress: true
12 |
13 | env:
14 | RAWG_API_KEY: ${{ secrets.RAWG_API_KEY }}
15 | BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
16 |
17 | jobs:
18 | androidTest:
19 | runs-on: ubuntu-latest # enables hardware acceleration in the virtual machine
20 | timeout-minutes: 30
21 | strategy:
22 | matrix:
23 | api-level: [ 29, 30, 31, 33, 34 ]
24 |
25 | steps:
26 | - name: Enable KVM group perms
27 | run: |
28 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
29 | sudo udevadm control --reload-rules
30 | sudo udevadm trigger --name-match=kvm
31 | ls /dev/kvm
32 |
33 | - name: Checkout
34 | uses: actions/checkout@v3
35 | with:
36 | fetch-depth: 0
37 |
38 | - name: set up JDK 17
39 | uses: actions/setup-java@v3
40 | with:
41 | java-version: '17'
42 | distribution: 'zulu'
43 |
44 | - name: Setup Gradle
45 | uses: gradle/gradle-build-action@v2.7.0
46 |
47 | - name: Grant execute permission for gradlew
48 | run: chmod +x gradlew
49 |
50 | - name: Run instrumentation tests
51 | uses: reactivecircus/android-emulator-runner@v2
52 | with:
53 | api-level: ${{ matrix.api-level }}
54 | arch: x86_64
55 | target: google_apis
56 | disable-animations: true
57 | disk-size: 6000M
58 | heap-size: 600M
59 | script: ./gradlew connectedDebugAndroidTest --stacktrace --daemon
60 |
61 | - name: Upload test reports
62 | if: always()
63 | uses: actions/upload-artifact@v4
64 | with:
65 | name: test-reports-${{ matrix.api-level }}
66 | path: ./**/build/reports
67 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and run local tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | tags-ignore:
8 | - 'v*'
9 | paths-ignore:
10 | - '**.md'
11 | - '**.txt'
12 | pull_request:
13 |
14 | concurrency:
15 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
16 | cancel-in-progress: true
17 |
18 | env:
19 | RAWG_API_KEY: ${{ secrets.RAWG_API_KEY }}
20 | BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
21 |
22 | jobs:
23 | build:
24 | runs-on: ubuntu-latest
25 |
26 | steps:
27 | - name: Checkout
28 | uses: actions/checkout@v3
29 |
30 | - name: Validate Gradle Wrapper
31 | uses: gradle/wrapper-validation-action@v1
32 |
33 | - name: set up JDK 17
34 | uses: actions/setup-java@v3
35 | with:
36 | java-version: '17'
37 | distribution: 'temurin'
38 | cache: gradle
39 |
40 | - name: Setup Gradle
41 | uses: gradle/gradle-build-action@v2.7.0
42 |
43 | - name: Grant execute permission for gradlew
44 | run: chmod +x gradlew
45 |
46 | - name: Build debug
47 | run: ./gradlew assembleDebug packageDistributionForCurrentOS --stacktrace
48 |
49 | - name: Run local tests
50 | run: ./gradlew testDebug desktopTest --stacktrace
51 |
52 | - name: Apply ktlint formatting to (.kt/s) files.
53 | run: ./gradlew ktlintFormat --stacktrace
54 |
55 | - name: Apply Spotless formatting if not applied.
56 | run: ./gradlew spotlessApply --stacktrace
57 |
58 | - name: Commit and push changes (if any).
59 | if: ${{ github.ref == 'refs/heads/main' }}
60 | uses: EndBug/add-and-commit@v9
61 | with:
62 | author_name: GitHub Actions
63 | author_email: github-actions@github.com
64 | message: Apply style formatting
65 | push: true
66 |
67 | - name: Upload build outputs (APKs & Desktop binaries)
68 | uses: actions/upload-artifact@v4
69 | with:
70 | name: build-outputs
71 | path: |
72 | **/build/outputs/*
73 | **/build/compose/*
74 |
75 | - name: Upload build reports
76 | if: always()
77 | uses: actions/upload-artifact@v4
78 | with:
79 | name: build-reports
80 | path: ./**/build/reports
81 |
--------------------------------------------------------------------------------
/.github/workflows/refreshVersions.yml:
--------------------------------------------------------------------------------
1 | name: RefreshVersions
2 |
3 | on:
4 | schedule:
5 | - cron: '0 7 * * 1'
6 |
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
9 | cancel-in-progress: true
10 |
11 | jobs:
12 | "Refresh-Versions":
13 | runs-on: "ubuntu-latest"
14 | steps:
15 | - id: step-0
16 | name: check-out
17 | uses: actions/checkout@v3
18 | with:
19 | ref: main
20 | - id: step-1
21 | name: setup-java
22 | uses: actions/setup-java@v3
23 | with:
24 | java-version: 17
25 | distribution: adopt
26 | - id: step-2
27 | name: create-branch
28 | uses: peterjgrainger/action-create-branch@v2.2.0
29 | with:
30 | branch: dependency-update
31 | env:
32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33 | - id: step-3
34 | name: gradle refreshVersions
35 | uses: gradle/gradle-build-action@v2.7.0
36 | with:
37 | arguments: refreshVersions
38 | - id: step-4
39 | name: Commit
40 | uses: EndBug/add-and-commit@v9
41 | with:
42 | author_name: GitHub Actions
43 | author_email: noreply@github.com
44 | message: Refresh versions.properties
45 | new_branch: dependency-update
46 | push: --force --set-upstream origin dependency-update
47 | - id: step-5
48 | name: Pull Request
49 | uses: repo-sync/pull-request@v2
50 | with:
51 | source_branch: dependency-update
52 | destination_branch: main
53 | pr_title: Upgrade gradle dependencies
54 | pr_body: '[refreshVersions](https://github.com/jmfayard/refreshVersions) has found those library updates!'
55 | pr_draft: true
56 | github_token: ${{ secrets.GITHUB_TOKEN }}
57 |
--------------------------------------------------------------------------------
/.github/workflows/release-desktop.yml:
--------------------------------------------------------------------------------
1 | name: Release a new Desktop app version
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*-desktop'
7 |
8 | jobs:
9 | release:
10 | strategy:
11 | matrix:
12 | os: [ubuntu-latest, macos-latest, windows-latest]
13 | runs-on: ${{ matrix.os }}
14 | permissions:
15 | contents: write
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | with:
20 | fetch-depth: 0
21 |
22 | - name: Validate Gradle Wrapper
23 | uses: gradle/wrapper-validation-action@v1
24 |
25 | - name: set up JDK 17
26 | uses: actions/setup-java@v3
27 | with:
28 | java-version: '17'
29 | distribution: 'temurin'
30 | cache: gradle
31 |
32 | - name: Setup Gradle
33 | uses: gradle/gradle-build-action@v2.7.0
34 |
35 | - name: Grant execute permission for gradlew
36 | run: chmod +x gradlew
37 |
38 | - name: Build Desktop release binary
39 | run: ./gradlew packageDistributionForCurrentOS --stacktrace
40 |
41 | - name: Upload Desktop release binary
42 | uses: actions/upload-artifact@v4
43 | with:
44 | name: desktopApp-${{ matrix.os }}-release
45 | path: desktopApp/build/compose/binaries/main/
46 |
47 | - uses: ncipollo/release-action@v1
48 | with:
49 | artifacts: "./desktopApp/build/compose/binaries/main/*/*.*"
50 | generateReleaseNotes: true
51 | allowUpdates: true
52 | omitNameDuringUpdate: true
53 | omitBodyDuringUpdate: true
54 | replacesArtifacts: false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | /.idea/*
11 | !/.idea/codeStyles/
12 | .DS_Store
13 | /build
14 | /captures
15 | .externalNativeBuild
16 | .cxx
17 | local.properties
18 | keystore.properties
19 | play_config.json
20 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Development Setup:
2 | - You need JDK 17 or higher.
3 | - Go to https://rawg.io/apidocs to Obtain your API key.
4 | - Add `RAWG_API_KEY=` to local.properties file, and replace `` with your obtained API key.
5 | - Run `./gradlew generateNonAndroidBuildConfig`.
6 |
7 | ## Submit A contribution:
8 | 1. Open an issue describing the bug or the feature request.
9 | 2. Fork the repository.
10 | 3. Make a new branch `git checkout -b new_feature`.
11 | 4. Make your changes.
12 | 5. Commit changes, `git add .` then `git commit -m "Commit Message"`.
13 | 6. push the changes upstream to your fork `git push origin new_feature`.
14 | 7. Open a PR to merge the changes with the original Repository.
15 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | 1. Update google play's release notes located in androidApp/src/main/play/release-notes/en-US/[track].txt.
2 | 2.
3 | - Add new git tag locally `git tag ` & push it to remote origin `git push origin `.
4 | - Add the same tag suffixed with '-desktop' & push it to remote as the previous step.
5 |
6 | That's it.
--------------------------------------------------------------------------------
/androidApp/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /release
--------------------------------------------------------------------------------
/androidApp/google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "807285618590",
4 | "project_id": "ludi-d1698",
5 | "storage_bucket": "ludi-d1698.appspot.com"
6 | },
7 | "client": [
8 | {
9 | "client_info": {
10 | "mobilesdk_app_id": "1:807285618590:android:aa1d5f1dbf1bca09b5888f",
11 | "android_client_info": {
12 | "package_name": "com.mr3y.ludi"
13 | }
14 | },
15 | "oauth_client": [
16 | {
17 | "client_id": "807285618590-nqrmjqb4o9nscfmlckpibsrmntrbd0kf.apps.googleusercontent.com",
18 | "client_type": 1,
19 | "android_info": {
20 | "package_name": "com.mr3y.ludi",
21 | "certificate_hash": "2763bb5905c1cefac327aeae2a28c2950459b697"
22 | }
23 | },
24 | {
25 | "client_id": "807285618590-uf86l1m9655g5mlgaapm8qat3e0r4bqc.apps.googleusercontent.com",
26 | "client_type": 3
27 | }
28 | ],
29 | "api_key": [
30 | {
31 | "current_key": "AIzaSyAl-A_L3jVe8JZ3L0u2ynAW1nbaBvXTVQc"
32 | }
33 | ],
34 | "services": {
35 | "appinvite_service": {
36 | "other_platform_oauth_client": [
37 | {
38 | "client_id": "807285618590-uf86l1m9655g5mlgaapm8qat3e0r4bqc.apps.googleusercontent.com",
39 | "client_type": 3
40 | }
41 | ]
42 | }
43 | }
44 | }
45 | ],
46 | "configuration_version": "1"
47 | }
--------------------------------------------------------------------------------
/androidApp/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | -verbose
9 | -allowaccessmodification
10 | -repackageclasses
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | -keepattributes SourceFile,LineNumberTable
22 |
23 | -assumenosideeffects class com.mr3y.ludi.shared.core.internal.KermitLogger {
24 | public *** v(...);
25 | public *** d(...);
26 | public *** e(...);
27 | public *** i(...);
28 | public *** w(...);
29 | }
30 |
31 | -dontwarn org.slf4j.impl.StaticLoggerBinder
32 |
33 | -keepclasseswithmembernames class com.mr3y.ludi.datastore.model.** { *; }
34 |
35 | # TODO: remove these rules once https://github.com/square/retrofit/issues/3751 is solved.
36 | #-keep,allowobfuscation,allowshrinking interface retrofit2.Call
37 | #-keep,allowobfuscation,allowshrinking class retrofit2.Response
38 |
39 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
40 |
41 | # Wire
42 | -keep class com.squareup.wire.** { *; }
43 |
44 | # If you keep the line number information, uncomment this to
45 | # hide the original source file name.
46 | -renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/androidApp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
11 |
12 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/androidApp/src/main/kotlin/com/mr3y/ludi/LudiApplication.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi
2 |
3 | import android.app.Application
4 | import com.mr3y.ludi.shared.di.AndroidApplicationComponent
5 | import com.mr3y.ludi.shared.di.create
6 |
7 | class LudiApplication : Application() {
8 |
9 | val component: AndroidApplicationComponent by lazy(LazyThreadSafetyMode.NONE) {
10 | AndroidApplicationComponent::class.create(this)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/androidApp/src/main/kotlin/com/mr3y/ludi/ui/preview/LudiPreview.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.ui.preview
2 |
3 | import android.content.res.Configuration.UI_MODE_NIGHT_NO
4 | import android.content.res.Configuration.UI_MODE_NIGHT_YES
5 | import androidx.compose.ui.tooling.preview.Preview
6 | import androidx.compose.ui.tooling.preview.Wallpapers
7 |
8 | @Preview(name = "Light & Dynamic colors disabled", locale = "en-US", device = "id:pixel_6", uiMode = UI_MODE_NIGHT_NO)
9 | @Preview(name = "Dark & Dynamic colors disabled", locale = "en-US", device = "id:pixel_6", uiMode = UI_MODE_NIGHT_YES)
10 | @Preview(name = "Light & Dynamic Colors enabled", locale = "en-US", device = "id:pixel_6", uiMode = UI_MODE_NIGHT_NO, wallpaper = Wallpapers.GREEN_DOMINATED_EXAMPLE)
11 | @Preview(name = "Dark & Dynamic Colors enabled", locale = "en-US", device = "id:pixel_6", uiMode = UI_MODE_NIGHT_YES, wallpaper = Wallpapers.GREEN_DOMINATED_EXAMPLE)
12 | annotation class LudiPreview
13 |
--------------------------------------------------------------------------------
/androidApp/src/main/play/release-notes/en-US/beta.txt:
--------------------------------------------------------------------------------
1 | - Internal changes to support latest android versions.
--------------------------------------------------------------------------------
/androidApp/src/main/res/drawable-night/ic_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
14 |
17 |
18 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/drawable/ic_splash.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
13 |
16 |
17 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-anydpi-v33/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-hdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-hdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-mdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-mdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/androidApp/src/main/res/values-night-v27/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
16 |
17 |
20 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/values-v27/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF1960A5
4 | #FFFFFFFF
5 | #FF7B5900
6 | #FFFFFFFF
7 | #FFFDFCFF
8 | #FF1A1C1E
9 | #FFA3C9FF
10 | #FF00315D
11 | #FFF8BD39
12 | #FF412D00
13 | #FF1A1C1E
14 | #FFE3E2E6
15 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Ludi
3 | strikt.api.expectThat
4 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
17 |
18 |
21 |
22 |
26 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | // this is necessary to avoid the plugins to be loaded multiple times
4 | // in each subproject's classloader
5 | alias(libs.plugins.kotlin.multiplatform).apply(false)
6 | alias(libs.plugins.kotlin.jvm).apply(false)
7 | alias(libs.plugins.kotlin.android).apply(false)
8 | alias(libs.plugins.android.application).apply(false)
9 | alias(libs.plugins.android.library).apply(false)
10 | alias(libs.plugins.compose.multiplatform).apply(false)
11 | alias(libs.plugins.compose.compiler).apply(false)
12 | alias(libs.plugins.spotless.plugin).apply(false)
13 | alias(libs.plugins.ktlint.plugin).apply(false)
14 | }
--------------------------------------------------------------------------------
/changelog_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "categories": [
3 | {
4 | "title": "## 🚀 Features",
5 | "labels": ["feature", "enhancement"]
6 | },
7 | {
8 | "title": "## 🐛 Bugs",
9 | "labels": ["bug"]
10 | },
11 | {
12 | "title": "## 🔧 Internals",
13 | "labels": ["Tech dept"]
14 | }
15 | ],
16 | "ignore_labels": [
17 | "duplicate", "good first issue", "help wanted", "invalid", "question", "wontfix", "skip release notes", "hold"
18 | ],
19 | "sort": "ASC",
20 | "template": "${{CHANGELOG}}",
21 | "pr_template": "- ${{TITLE}} (#${{NUMBER}})",
22 | "empty_template": "- no changes",
23 | "transformers": [
24 | {
25 | "pattern": "[\\-\\*] (\\[(...|TEST|CI|SKIP)\\])( )?(.+?)\n(.+?[\\-\\*] )(.+)",
26 | "target": "- $4\n - $6"
27 | }
28 | ],
29 | "max_tags_to_fetch": 200,
30 | "max_pull_requests": 200,
31 | "max_back_track_time_days": 365,
32 | "tag_resolver": {
33 | "method": "semver"
34 | },
35 | "base_branches": [
36 | "main"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/convention-plugins/gradle.properties:
--------------------------------------------------------------------------------
1 | # Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534
2 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
3 | kotlin.code.style=official
4 | org.gradle.caching=true
5 | org.gradle.parallel=true
--------------------------------------------------------------------------------
/convention-plugins/plugins/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/convention-plugins/plugins/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2 |
3 | plugins {
4 | `kotlin-dsl`
5 | }
6 |
7 | group = "com.mr3y.ludi.conventionplugins"
8 |
9 | java {
10 | sourceCompatibility = JavaVersion.VERSION_17
11 | targetCompatibility = JavaVersion.VERSION_17
12 | }
13 |
14 | tasks.withType().configureEach {
15 | kotlinOptions {
16 | jvmTarget = JavaVersion.VERSION_17.toString()
17 | }
18 | }
19 |
20 | dependencies {
21 | compileOnly(libs.android.gradlePlugin)
22 | compileOnly(libs.spotless.gradlePlugin)
23 | compileOnly(libs.ktlint.gradlePlugin)
24 | }
25 |
26 | gradlePlugin {
27 | plugins {
28 | register("commonConventionPlugin") {
29 | id = "ludi.common"
30 | implementationClass = "com.mr3y.ludi.gradle.CommonConventionPlugin"
31 | }
32 | register("androidConventionPlugin") {
33 | id = "ludi.android.common"
34 | implementationClass = "com.mr3y.ludi.gradle.AndroidConventionPlugin"
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/convention-plugins/plugins/src/main/kotlin/com/mr3y/ludi/gradle/AndroidConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.gradle
2 |
3 | import com.android.build.api.dsl.CommonExtension
4 | import com.android.build.gradle.LibraryExtension
5 | import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
6 | import org.gradle.api.JavaVersion
7 | import org.gradle.api.Plugin
8 | import org.gradle.api.Project
9 | import org.gradle.kotlin.dsl.getByType
10 |
11 | class AndroidConventionPlugin : Plugin {
12 |
13 | override fun apply(target: Project) {
14 | with(target) {
15 | pluginManager.withPlugin("com.android.library") {
16 | val androidLibExtension = extensions.getByType()
17 | androidLibExtension.apply {
18 | applyCommonAndroidConvention()
19 | }
20 | }
21 | pluginManager.withPlugin("com.android.application") {
22 | val androidAppExtension = extensions.getByType()
23 | androidAppExtension.apply {
24 | applyCommonAndroidConvention()
25 | }
26 | }
27 | }
28 | }
29 |
30 | private fun CommonExtension<*, *, *, *, *, *>.applyCommonAndroidConvention() {
31 | compileSdk = 36
32 |
33 | defaultConfig {
34 | minSdk = 26
35 |
36 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
37 | }
38 |
39 | compileOptions {
40 | sourceCompatibility = JavaVersion.VERSION_17
41 | targetCompatibility = JavaVersion.VERSION_17
42 | }
43 |
44 | buildFeatures {
45 | compose = true
46 | aidl = false
47 | buildConfig = false
48 | renderScript = false
49 | shaders = false
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/convention-plugins/plugins/src/main/kotlin/com/mr3y/ludi/gradle/CommonConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.gradle
2 |
3 | import com.diffplug.gradle.spotless.SpotlessExtension
4 | import org.gradle.api.Plugin
5 | import org.gradle.api.Project
6 | import org.gradle.kotlin.dsl.getByType
7 | import org.jlleitschuh.gradle.ktlint.KtlintExtension
8 |
9 | class CommonConventionPlugin : Plugin {
10 |
11 | override fun apply(target: Project) {
12 | with(target) {
13 | pluginManager.apply("org.jlleitschuh.gradle.ktlint")
14 | pluginManager.apply("com.diffplug.spotless")
15 |
16 | val ktlintExtension = extensions.getByType()
17 | val spotlessExtension = extensions.getByType()
18 | configureKtlintExtension(ktlintExtension)
19 | configureSpotlessExtension(spotlessExtension)
20 | }
21 | }
22 |
23 | private fun Project.configureKtlintExtension(extension: KtlintExtension) {
24 | extension.apply {
25 | filter {
26 | exclude {
27 | it.file.path.contains("build")
28 | }
29 | }
30 | }
31 | }
32 |
33 | private fun Project.configureSpotlessExtension(extension: SpotlessExtension) {
34 | extension.apply {
35 | format("misc") {
36 | // define the files to apply `misc` to
37 | target(listOf("**/*.gradle", "*.md", ".gitignore", "**/*.gradle.kts"))
38 |
39 | // define the steps to apply to those files
40 | trimTrailingWhitespace()
41 | indentWithSpaces(4)
42 | endWithNewline()
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/convention-plugins/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | maven {
6 | url = uri("https://plugins.gradle.org/m2/")
7 | content {
8 | includeGroup("org.jlleitschuh.gradle")
9 | }
10 | }
11 | }
12 | versionCatalogs {
13 | create("libs") {
14 | from(files("../gradle/libs.versions.toml"))
15 | }
16 | }
17 | }
18 |
19 | rootProject.name = "convention-plugins"
20 | include(":plugins")
--------------------------------------------------------------------------------
/desktopApp/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/desktopApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
3 |
4 | plugins {
5 | alias(libs.plugins.kotlin.multiplatform)
6 | alias(libs.plugins.compose.multiplatform)
7 | alias(libs.plugins.compose.compiler)
8 | alias(libs.plugins.ludi.common)
9 | }
10 |
11 | kotlin {
12 | jvm {
13 | withJava()
14 | }
15 | jvmToolchain(17)
16 | sourceSets {
17 | jvmMain {
18 | dependencies {
19 | implementation(compose.desktop.currentOs)
20 | implementation(compose.desktop.common)
21 | implementation(project(":shared"))
22 | }
23 | }
24 | }
25 | }
26 |
27 | dependencies {
28 | }
29 |
30 | tasks.withType {
31 | compilerOptions {
32 | jvmTarget = JvmTarget.JVM_17
33 | }
34 | }
35 |
36 | java {
37 | sourceCompatibility = JavaVersion.VERSION_17
38 | targetCompatibility = JavaVersion.VERSION_17
39 | }
40 |
41 | compose.desktop {
42 | sourceSets["main"].resources.srcDirs("src/jvmMain/resources")
43 | application {
44 | mainClass = "com.mr3y.ludi.desktop.MainKt"
45 |
46 | nativeDistributions {
47 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
48 | packageName = "com.mr3y.ludi.desktop"
49 |
50 | description = "Ludi is a Kotlin Multiplatform (Android + Desktop) app to demonstrate best practices & using modern technologies to develop high quality apps"
51 | copyright = "Apache License Version 2.0"
52 | vendor = "mr3y-the-programmer"
53 |
54 | buildTypes.release.proguard {
55 | isEnabled.set(true)
56 | configurationFiles.from("proguard-rules.pro")
57 | }
58 | windows {
59 | shortcut = true
60 | menu = true
61 | menuGroup = "Ludi"
62 | iconFile.set(file("src/jvmMain/resources/icon_light.ico"))
63 | }
64 | macOS {
65 | bundleID = "com.mr3y.ludi"
66 | packageName = "com.mr3y.ludi.desktop"
67 | // TODO: provide .icns icon
68 | iconFile.set(file("src/jvmMain/resources/icon_light.xml"))
69 | }
70 | linux {
71 | iconFile.set(file("src/jvmMain/resources/icon_light.png"))
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/desktopApp/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -verbose
2 | -allowaccessmodification
3 | -repackageclasses
4 |
5 | # Uncomment this to preserve the line number information for
6 | # debugging stack traces.
7 | -keepattributes SourceFile,LineNumberTable
8 |
9 | -assumenosideeffects class com.mr3y.ludi.shared.core.internal.KermitLogger {
10 | public *** v(...);
11 | public *** d(...);
12 | public *** e(...);
13 | public *** i(...);
14 | public *** w(...);
15 | }
16 |
17 | -dontwarn org.slf4j.impl.StaticLoggerBinder
18 |
19 | -keepclasseswithmembernames class com.mr3y.ludi.datastore.model.** { *; }
20 |
21 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
22 |
23 | # Wire
24 | -keep class com.squareup.wire.** { *; }
25 |
26 | # If you keep the line number information, uncomment this to
27 | # hide the original source file name.
28 | -renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/desktopApp/src/jvmMain/kotlin/com/mr3y/ludi/desktop/Main.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.desktop
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.ui.res.painterResource
8 | import androidx.compose.ui.window.Window
9 | import androidx.compose.ui.window.application
10 | import com.mr3y.ludi.shared.App
11 | import com.mr3y.ludi.shared.di.DesktopApplicationComponent
12 | import com.mr3y.ludi.shared.di.HostWindowComponent
13 | import com.mr3y.ludi.shared.di.LocalHostWindowComponent
14 | import com.mr3y.ludi.shared.di.create
15 | import com.mr3y.ludi.shared.ui.presenter.model.Theme
16 |
17 | fun main() {
18 | val appComponent = DesktopApplicationComponent::class.create()
19 | application {
20 | val preferences by appComponent.appState.preferences.collectAsState()
21 | Window(
22 | title = "Ludi",
23 | onCloseRequest = ::exitApplication,
24 | icon = painterResource("icon_light.xml")
25 | ) {
26 | val windowComponent = HostWindowComponent::class.create(window, appComponent)
27 | CompositionLocalProvider(
28 | LocalHostWindowComponent provides windowComponent
29 | ) {
30 | if (preferences != null) {
31 | App(
32 | isDarkTheme = when (preferences!!.theme) {
33 | Theme.Light -> false
34 | Theme.Dark -> true
35 | Theme.SystemDefault -> isSystemInDarkTheme()
36 | },
37 | useDynamicColor = preferences!!.useDynamicColor,
38 | showOnboardingScreen = preferences!!.showOnBoardingScreen
39 | )
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/desktopApp/src/jvmMain/resources/icon_light.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/desktopApp/src/jvmMain/resources/icon_light.ico
--------------------------------------------------------------------------------
/desktopApp/src/jvmMain/resources/icon_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/desktopApp/src/jvmMain/resources/icon_light.png
--------------------------------------------------------------------------------
/desktopApp/src/jvmMain/resources/icon_light.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
13 |
16 |
17 |
--------------------------------------------------------------------------------
/docs/PrivacyPolicy.md:
--------------------------------------------------------------------------------
1 | # Ludi Privacy Policy - Developer: Abdelrahman Khairy
2 | ## Permissions Ludi has:
3 | Ludi has the following permissions in order to function correctly
4 | - android.permission.INTERNET
5 | - android.permission.ACCESS_NETWORK_STATE
6 |
7 | ## Third-party SDKs Ludi uses:
8 | Ludi uses Google Crashlytics and Analytics to gather data about crashes happening while the app is being used by users, this data is
9 | collected to help me fix Crashes & bugs, and hence Improve the end user experience, All data is anonymous.
10 |
11 | If you think the app is accessing or collecting data that is not supposed to be accessed or collected,
12 | then reach out at 26522145+mr3y-the-programmer@users.noreply.github.com.
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | [PrivacyPolicy](https://github.com/mr3y-the-programmer/Ludi/blob/main/docs/PrivacyPolicy.md)
2 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.enableR8.fullMode=true
25 | org.gradle.caching=true
26 | org.gradle.parallel=true
27 | # kspCommonMainKotlinMetadata is incompatible with configuration caching.
28 | org.gradle.unsafe.configuration-cache=false
29 |
30 | #KMP
31 | kotlin.mpp.androidSourceSetLayoutVersion=2
32 | kotlin.mpp.stability.nowarn=true
33 | kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Feb 26 22:07:05 EET 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
4 | distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa
5 | distributionPath=wrapper/dists
6 | zipStorePath=wrapper/dists
7 | zipStoreBase=GRADLE_USER_HOME
8 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Ludi
5 |
6 |
7 | Privacy Policy
8 |
9 |
10 |
--------------------------------------------------------------------------------
/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.net.URI
2 |
3 | pluginManagement {
4 | includeBuild("convention-plugins")
5 | repositories {
6 | google()
7 | mavenCentral()
8 | gradlePluginPortal()
9 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
10 | }
11 | plugins {
12 | id("de.fayard.refreshVersions") version "0.60.5"
13 | }
14 | }
15 | buildscript {
16 | // Workaround for: https://github.com/Splitties/refreshVersions/issues/707
17 | dependencies {
18 | classpath("com.squareup.okio:okio:3.9.0")
19 | }
20 | }
21 | plugins {
22 | id("de.fayard.refreshVersions")
23 | }
24 |
25 | refreshVersions {
26 | rejectVersionIf {
27 | // Recent versions of ktlint gradle plugin changed the default
28 | // code convention style which affects nearly all files in the codebase,
29 | // so, for now we are rejecting updates, as we are fine with the current style.
30 | val blacklist = listOf("org.jlleitschuh.gradle.ktlint")
31 | candidate.stabilityLevel.isLessStableThan(current.stabilityLevel) || moduleId.group in blacklist || candidate.value.endsWith("-jre") || candidate.value.endsWith("-1.8.20")
32 | }
33 | }
34 |
35 | dependencyResolutionManagement {
36 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
37 | repositories {
38 | google()
39 | mavenCentral()
40 | maven { url = URI.create("https://jitpack.io") }
41 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
42 | }
43 | }
44 | rootProject.name = "Ludi"
45 | include(":androidApp")
46 | include(":shared")
47 | include(":desktopApp")
--------------------------------------------------------------------------------
/shared/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/shared/src/androidInstrumentedTest/kotlin/com/mr3y/ludi/shared/ui/presenter/MainDispatcherRule.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.presenter
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.ExperimentalCoroutinesApi
5 | import kotlinx.coroutines.test.TestDispatcher
6 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
7 | import kotlinx.coroutines.test.resetMain
8 | import kotlinx.coroutines.test.setMain
9 | import org.junit.rules.TestWatcher
10 | import org.junit.runner.Description
11 |
12 | /**
13 | * A JUnit [TestRule] that sets the Main dispatcher to [testDispatcher]
14 | * for the duration of the test.
15 | */
16 | @OptIn(ExperimentalCoroutinesApi::class)
17 | class MainDispatcherRule(
18 | val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
19 | ) : TestWatcher() {
20 | override fun starting(description: Description) {
21 | Dispatchers.setMain(testDispatcher)
22 | }
23 |
24 | override fun finished(description: Description) {
25 | Dispatchers.resetMain()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/shared/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/core/CrashlyticsReporting.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core
2 |
3 | import com.google.firebase.crashlytics.FirebaseCrashlytics
4 | import me.tatarka.inject.annotations.Inject
5 |
6 | @Inject
7 | class CrashlyticsReporting(
8 | private val crashlytics: FirebaseCrashlytics
9 | ) : CrashReporting {
10 |
11 | override fun recordException(throwable: Throwable, logMessage: String?) {
12 | with(crashlytics) {
13 | if (logMessage != null) {
14 | log(logMessage)
15 | }
16 | recordException(throwable)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/core/database/DatabaseFactory.android.kt:
--------------------------------------------------------------------------------
1 | // ktlint-disable filename
2 | package com.mr3y.ludi.shared.core.database
3 |
4 | import android.content.Context
5 | import app.cash.sqldelight.async.coroutines.synchronous
6 | import app.cash.sqldelight.db.SqlDriver
7 | import app.cash.sqldelight.driver.android.AndroidSqliteDriver
8 | import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
9 | import me.tatarka.inject.annotations.Inject
10 |
11 | @Inject
12 | actual class DriverFactory(private val applicationContext: Context) {
13 | actual fun createDriver(): SqlDriver {
14 | return AndroidSqliteDriver(
15 | schema = LudiDatabase.Schema.synchronous(),
16 | context = applicationContext,
17 | name = "ludi_database.db",
18 | factory = RequerySQLiteOpenHelperFactory()
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/AndroidApplicationComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import android.content.Context
4 | import com.mr3y.ludi.shared.di.annotations.Singleton
5 | import me.tatarka.inject.annotations.Component
6 | import me.tatarka.inject.annotations.Provides
7 | import okio.Path
8 | import okio.Path.Companion.toOkioPath
9 | import java.io.File
10 |
11 | @Component
12 | @Singleton
13 | abstract class AndroidApplicationComponent(
14 | @get:Provides val applicationContext: Context
15 | ) : SharedApplicationComponent, AndroidCrashReportingComponent, AndroidSqlDriverComponent {
16 |
17 | override val dataStoreParentDir: Path = applicationContext.filesDir.toOkioPath()
18 |
19 | override val okhttpCacheParentDir: File = applicationContext.cacheDir
20 |
21 | companion object
22 | }
23 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/AndroidCrashReportingComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.google.firebase.crashlytics.FirebaseCrashlytics
4 | import com.google.firebase.crashlytics.ktx.crashlytics
5 | import com.google.firebase.ktx.Firebase
6 | import com.mr3y.ludi.shared.core.CrashReporting
7 | import com.mr3y.ludi.shared.core.CrashlyticsReporting
8 | import com.mr3y.ludi.shared.di.annotations.Singleton
9 | import me.tatarka.inject.annotations.Provides
10 |
11 | interface AndroidCrashReportingComponent {
12 | @Singleton
13 | @Provides
14 | fun provideCrashlyticsInstance(): FirebaseCrashlytics {
15 | return Firebase.crashlytics
16 | }
17 |
18 | @Singleton
19 | @Provides
20 | fun provideCrashReportingInstance(crashlytics: FirebaseCrashlytics): CrashReporting {
21 | return CrashlyticsReporting(crashlytics)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/AndroidSqlDriverComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import android.content.Context
4 | import com.mr3y.ludi.shared.core.database.DriverFactory
5 | import com.mr3y.ludi.shared.di.annotations.Singleton
6 | import me.tatarka.inject.annotations.Provides
7 |
8 | interface AndroidSqlDriverComponent {
9 |
10 | @Singleton
11 | @Provides
12 | fun provideSqlDriverFactoryInstance(applicationContext: Context): DriverFactory {
13 | return DriverFactory(applicationContext)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/DatabaseDispatcherComponent.android.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.Singleton
4 | import kotlinx.coroutines.Dispatchers
5 | import me.tatarka.inject.annotations.Provides
6 |
7 | actual interface DatabaseDispatcherComponent {
8 |
9 | @Singleton
10 | @Provides
11 | fun provideIODatabaseDispatcher(): DatabaseDispatcher {
12 | return DatabaseDispatcher(Dispatchers.IO)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/DealsFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.DealsFeatureScope
4 | import me.tatarka.inject.annotations.Component
5 |
6 | @Component
7 | @DealsFeatureScope
8 | abstract class DealsFeatureComponent(
9 | @Component val parent: HostActivityComponent
10 | ) : SharedDealsFeatureComponent
11 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/DiscoverFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.DiscoverFeatureScope
4 | import me.tatarka.inject.annotations.Component
5 |
6 | @Component
7 | @DiscoverFeatureScope
8 | abstract class DiscoverFeatureComponent(
9 | @Component val parent: HostActivityComponent
10 | ) : SharedDiscoverFeatureComponent
11 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/HostActivityComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import android.app.Activity
4 | import com.mr3y.ludi.shared.ui.navigation.PreferencesType
5 | import com.mr3y.ludi.shared.ui.presenter.EditPreferencesViewModel
6 | import me.tatarka.inject.annotations.Component
7 | import me.tatarka.inject.annotations.Provides
8 |
9 | @Component
10 | abstract class HostActivityComponent(
11 | @get:Provides val activity: Activity,
12 | @Component val parent: AndroidApplicationComponent
13 | ) {
14 | abstract val editPreferencesViewModelFactory: (PreferencesType) -> EditPreferencesViewModel
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/HostActivityComponentOwner.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | interface HostActivityComponentOwner {
4 |
5 | val hostActivityComponent: HostActivityComponent
6 | }
7 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/NewsFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.NewsFeatureScope
4 | import me.tatarka.inject.annotations.Component
5 |
6 | @Component
7 | @NewsFeatureScope
8 | abstract class NewsFeatureComponent(
9 | @Component val parent: HostActivityComponent
10 | ) : SharedNewsFeatureComponent
11 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/OnboardingFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.OnboardingFeatureScope
4 | import me.tatarka.inject.annotations.Component
5 |
6 | @Component
7 | @OnboardingFeatureScope
8 | abstract class OnboardingFeatureComponent(
9 | @Component val parent: HostActivityComponent
10 | ) : SharedOnboardingFeatureComponent
11 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/SettingsFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.SettingsFeatureScope
4 | import me.tatarka.inject.annotations.Component
5 |
6 | @Component
7 | @SettingsFeatureScope
8 | abstract class SettingsFeatureComponent(
9 | @Component val parent: HostActivityComponent
10 | ) : SharedSettingsFeatureComponent
11 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/di/TimeSourceComponent.android.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import android.os.SystemClock
4 | import com.mr3y.ludi.shared.di.annotations.Singleton
5 | import me.tatarka.inject.annotations.Provides
6 | import kotlin.time.AbstractLongTimeSource
7 | import kotlin.time.DurationUnit
8 | import kotlin.time.TimeSource
9 |
10 | actual interface TimeSourceComponent {
11 |
12 | @Provides
13 | @Singleton
14 | fun provideTimeSourceInstance(): TimeSource {
15 | return object : AbstractLongTimeSource(DurationUnit.NANOSECONDS) {
16 | override fun read(): Long = SystemClock.elapsedRealtimeNanos()
17 |
18 | override fun toString(): String = "TimeSource(SystemClock.elapsedRealtimeNanos())"
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/ui/components/LudiAsyncImage.android.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components
2 |
3 | import androidx.compose.ui.graphics.ImageBitmap
4 | import androidx.compose.ui.graphics.asImageBitmap
5 | import coil3.Bitmap
6 | import coil3.request.ImageRequest
7 | import coil3.request.allowHardware
8 |
9 | actual fun ImageRequest.Builder.platformSpecificConfig(allowHardware: Boolean): ImageRequest.Builder = this.allowHardware(allowHardware)
10 |
11 | actual fun Bitmap.asImageBitmap(): ImageBitmap = this.asImageBitmap()
12 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/ui/components/LudiChromeCustomTab.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import androidx.annotation.ColorInt
6 | import androidx.browser.customtabs.CustomTabColorSchemeParams
7 | import androidx.browser.customtabs.CustomTabsIntent
8 | import androidx.compose.material3.ColorScheme
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.ReadOnlyComposable
11 | import androidx.compose.ui.graphics.toArgb
12 |
13 | val ColorScheme.chromeCustomTabToolbarColor: Int
14 | @Composable
15 | @ReadOnlyComposable
16 | get() = primaryContainer.toArgb()
17 |
18 | fun launchChromeCustomTab(context: Context, url: Uri, @ColorInt toolbarColor: Int) {
19 | val customTabsIntent = CustomTabsIntent.Builder()
20 | .setDefaultColorSchemeParams(
21 | CustomTabColorSchemeParams.Builder()
22 | .setToolbarColor(toolbarColor)
23 | .build()
24 | )
25 | .build()
26 |
27 | customTabsIntent.launchUrl(context, url)
28 | }
29 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/ui/lifecycle/ConnectionState.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.lifecycle
2 |
3 | enum class ConnectionState {
4 | /**
5 | * Initial [ConnectionState] when we don't know yet about the availability of the internet connectivity on device.
6 | */
7 | Undefined,
8 | Available,
9 | Unavailable
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/ui/presenter/FrameClock.android.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.presenter
2 |
3 | import androidx.compose.ui.platform.AndroidUiDispatcher
4 | import kotlin.coroutines.CoroutineContext
5 |
6 | internal actual fun frameClock(): CoroutineContext = AndroidUiDispatcher.Main
7 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/ui/resources/PlatformCompositionProvider.android.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.resources
2 |
3 | actual fun isDesktopPlatform() = false
4 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/ui/screens/deals/DealsScreen.android.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.screens.deals
2 |
3 | import android.net.Uri
4 | import androidx.compose.foundation.layout.statusBarsPadding
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.platform.LocalContext
10 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
11 | import com.mr3y.ludi.shared.ui.components.chromeCustomTabToolbarColor
12 | import com.mr3y.ludi.shared.ui.components.launchChromeCustomTab
13 | import com.mr3y.ludi.shared.ui.presenter.DealsViewModel
14 |
15 | @Composable
16 | actual fun DealsScreen(
17 | modifier: Modifier,
18 | viewModel: DealsViewModel
19 | ) {
20 | val dealsState by viewModel.dealsState.collectAsStateWithLifecycle()
21 | val context = LocalContext.current
22 | val tabToolbarColor = MaterialTheme.colorScheme.chromeCustomTabToolbarColor
23 | DealsScreen(
24 | dealsState = dealsState,
25 | searchQuery = viewModel.searchQuery.value,
26 | modifier = modifier.statusBarsPadding(),
27 | onUpdateSearchQuery = viewModel::updateSearchQuery,
28 | onSelectingDealStore = viewModel::addToSelectedDealsStores,
29 | onUnselectingDealStore = viewModel::removeFromSelectedDealsStores,
30 | onSelectingGiveawayStore = viewModel::addToSelectedGiveawaysStores,
31 | onUnselectingGiveawayStore = viewModel::removeFromSelectedGiveawaysStores,
32 | onSelectingGiveawayPlatform = viewModel::addToSelectedGiveawaysPlatforms,
33 | onUnselectingGiveawayPlatform = viewModel::removeFromSelectedGiveawayPlatforms,
34 | onRefreshDeals = viewModel::refreshDeals,
35 | onRefreshDealsFinished = viewModel::refreshDealsComplete,
36 | onRefreshGiveaways = viewModel::refreshGiveaways,
37 | onSelectTab = viewModel::selectTab,
38 | onOpenUrl = { url ->
39 | launchChromeCustomTab(context, Uri.parse(url), tabToolbarColor)
40 | }
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/ui/screens/discover/DiscoverScreen.android.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.screens.discover
2 |
3 | import android.net.Uri
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.platform.LocalContext
9 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
10 | import com.mr3y.ludi.shared.ui.components.chromeCustomTabToolbarColor
11 | import com.mr3y.ludi.shared.ui.components.launchChromeCustomTab
12 | import com.mr3y.ludi.shared.ui.presenter.DiscoverViewModel
13 |
14 | @Composable
15 | actual fun DiscoverScreen(
16 | modifier: Modifier,
17 | viewModel: DiscoverViewModel
18 | ) {
19 | val discoverState by viewModel.discoverState.collectAsStateWithLifecycle()
20 | val context = LocalContext.current
21 | val tabToolbarColor = MaterialTheme.colorScheme.chromeCustomTabToolbarColor
22 | DiscoverScreen(
23 | discoverState = discoverState,
24 | searchQuery = viewModel.searchQuery.value,
25 | onUpdatingSearchQueryText = viewModel::updateSearchQuery,
26 | onSelectingPlatform = viewModel::addToSelectedPlatforms,
27 | onUnselectingPlatform = viewModel::removeFromSelectedPlatforms,
28 | onSelectingStore = viewModel::addToSelectedStores,
29 | onUnselectingStore = viewModel::removeFromSelectedStores,
30 | onSelectingTag = viewModel::addToSelectedTags,
31 | onUnselectingTag = viewModel::removeFromSelectedTags,
32 | onRefresh = viewModel::refresh,
33 | onRefreshFinished = viewModel::refreshComplete,
34 | onOpenUrl = { url ->
35 | launchChromeCustomTab(context, Uri.parse(url), tabToolbarColor)
36 | },
37 | modifier = modifier
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/ui/screens/news/NewsScreen.android.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.screens.news
2 |
3 | import android.net.Uri
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.platform.LocalContext
9 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
10 | import com.mr3y.ludi.shared.ui.components.chromeCustomTabToolbarColor
11 | import com.mr3y.ludi.shared.ui.components.launchChromeCustomTab
12 | import com.mr3y.ludi.shared.ui.presenter.NewsViewModel
13 |
14 | @Composable
15 | actual fun NewsScreen(
16 | onTuneClick: () -> Unit,
17 | modifier: Modifier,
18 | viewModel: NewsViewModel
19 | ) {
20 | val newsState by viewModel.newsState.collectAsStateWithLifecycle()
21 | val context = LocalContext.current
22 | val tabToolbarColor = MaterialTheme.colorScheme.chromeCustomTabToolbarColor
23 | NewsScreen(
24 | newsState = newsState,
25 | searchQuery = viewModel.searchQuery.value,
26 | onSearchQueryValueChanged = viewModel::updateSearchQuery,
27 | onTuneClick = onTuneClick,
28 | onRefresh = viewModel::refresh,
29 | onOpenUrl = { url ->
30 | launchChromeCustomTab(context, Uri.parse(url), tabToolbarColor)
31 | },
32 | onConsumeEvent = viewModel::consumeCurrentEvent,
33 | modifier = modifier
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/ui/screens/settings/SettingsScreen.android.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.screens.settings
2 |
3 | import android.net.Uri
4 | import android.os.Build
5 | import androidx.annotation.ChecksSdkIntAtLeast
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.platform.LocalContext
11 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
12 | import com.mr3y.ludi.shared.ui.components.chromeCustomTabToolbarColor
13 | import com.mr3y.ludi.shared.ui.components.launchChromeCustomTab
14 | import com.mr3y.ludi.shared.ui.presenter.SettingsViewModel
15 | import com.mr3y.ludi.shared.ui.theme.isDynamicColorSupported
16 |
17 | @Composable
18 | actual fun SettingsScreen(
19 | onFollowedNewsDataSourcesClick: () -> Unit,
20 | onFavouriteGamesClick: () -> Unit,
21 | onFavouriteGenresClick: () -> Unit,
22 | modifier: Modifier,
23 | viewModel: SettingsViewModel
24 | ) {
25 | val settingsState by viewModel.settingsState.collectAsStateWithLifecycle()
26 | val context = LocalContext.current
27 | val tabToolbarColor = MaterialTheme.colorScheme.chromeCustomTabToolbarColor
28 | SettingsScreen(
29 | settingsState,
30 | modifier = modifier,
31 | onFollowedNewsDataSourcesClick = onFollowedNewsDataSourcesClick,
32 | onFavouriteGamesClick = onFavouriteGamesClick,
33 | onFavouriteGenresClick = onFavouriteGenresClick,
34 | onUpdateTheme = viewModel::setAppTheme,
35 | onToggleDynamicColorValue = viewModel::enableUsingDynamicColor,
36 | onOpenUrl = { url ->
37 | launchChromeCustomTab(context, Uri.parse(url), tabToolbarColor)
38 | }
39 | )
40 | }
41 |
42 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
43 | actual fun isDynamicColorEnabled(): Boolean {
44 | return isDynamicColorSupported() && Build.VERSION.SDK_INT >= 31
45 | }
46 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/mr3y/ludi/shared/ui/theme/ColorScheme.android.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.material3.ColorScheme
5 | import androidx.compose.material3.dynamicDarkColorScheme
6 | import androidx.compose.material3.dynamicLightColorScheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.platform.LocalContext
9 |
10 | @Composable
11 | actual fun colorScheme(
12 | isDarkTheme: Boolean,
13 | useDynamicColors: Boolean
14 | ): ColorScheme {
15 | return when {
16 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && useDynamicColors -> {
17 | val context = LocalContext.current
18 | if (isDarkTheme) {
19 | dynamicDarkColorScheme(context)
20 | } else {
21 | dynamicLightColorScheme(context)
22 | }
23 | }
24 | else -> {
25 | if (isDarkTheme) {
26 | DarkColorScheme
27 | } else {
28 | LightColorScheme
29 | }
30 | }
31 | }
32 | }
33 |
34 | actual fun isDynamicColorSupported() = true
35 |
--------------------------------------------------------------------------------
/shared/src/androidUnitTest/kotlin/com/mr3y/ludi/shared/ui/screens/BaseRobolectricTest.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.screens
2 |
3 | import androidx.compose.ui.test.junit4.createComposeRule
4 | import org.junit.Before
5 | import org.junit.Rule
6 | import org.robolectric.shadows.ShadowLog
7 |
8 | open class BaseRobolectricTest {
9 | @get:Rule
10 | val composeTestRule = createComposeRule()
11 |
12 | @Before
13 | @Throws(Exception::class)
14 | fun setUp() {
15 | ShadowLog.stream = System.out // Redirect Logcat to console
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/shared/src/androidUnitTest/kotlin/com/mr3y/ludi/shared/ui/screens/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.screens
2 |
3 | import android.content.Context
4 | import androidx.compose.ui.semantics.Role
5 | import androidx.compose.ui.semantics.SemanticsProperties
6 | import androidx.compose.ui.test.SemanticsMatcher
7 | import androidx.compose.ui.test.SemanticsNodeInteraction
8 | import androidx.compose.ui.test.hasStateDescription
9 | import androidx.compose.ui.test.junit4.ComposeContentTestRule
10 | import androidx.test.core.app.ApplicationProvider
11 |
12 | internal val context: Context = ApplicationProvider.getApplicationContext()
13 |
14 | internal fun ComposeContentTestRule.onNodeWithStateDescription(
15 | stateDescription: String
16 | ): SemanticsNodeInteraction {
17 | return onNode(hasStateDescription(stateDescription))
18 | }
19 |
20 | internal fun hasRole(role: Role): SemanticsMatcher = SemanticsMatcher.expectValue(SemanticsProperties.Role, role)
21 |
--------------------------------------------------------------------------------
/shared/src/androidUnitTest/kotlin/com/mr3y/ludi/shared/ui/screens/onboarding/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.screens.onboarding
2 |
3 | import androidx.compose.ui.semantics.Role
4 | import androidx.compose.ui.test.SemanticsNodeInteraction
5 | import androidx.compose.ui.test.hasStateDescription
6 | import androidx.compose.ui.test.junit4.ComposeContentTestRule
7 | import com.mr3y.ludi.shared.ui.screens.hasRole
8 |
9 | internal fun ComposeContentTestRule.onGenre(genreStateDesc: String): SemanticsNodeInteraction {
10 | return onNode(hasStateDescription(genreStateDesc) and hasRole(Role.Checkbox))
11 | }
12 |
--------------------------------------------------------------------------------
/shared/src/commonMain/composeResources/drawable/game_spot_logo.xml:
--------------------------------------------------------------------------------
1 |
8 |
13 |
18 |
19 |
--------------------------------------------------------------------------------
/shared/src/commonMain/composeResources/drawable/gamerant_logo.xml:
--------------------------------------------------------------------------------
1 |
8 |
13 |
14 |
19 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/shared/src/commonMain/composeResources/drawable/ios.xml:
--------------------------------------------------------------------------------
1 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/shared/src/commonMain/composeResources/drawable/pc.xml:
--------------------------------------------------------------------------------
1 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/shared/src/commonMain/composeResources/drawable/placeholder.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
14 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/shared/src/commonMain/composeResources/drawable/xbox.xml:
--------------------------------------------------------------------------------
1 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/App.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
5 | import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.CompositionLocalProvider
8 | import androidx.compose.ui.Modifier
9 | import cafe.adriel.voyager.navigator.Navigator
10 | import coil3.ImageLoader
11 | import coil3.annotation.ExperimentalCoilApi
12 | import coil3.compose.setSingletonImageLoaderFactory
13 | import com.mr3y.ludi.shared.ui.adaptive.LocalWindowSizeClass
14 | import com.mr3y.ludi.shared.ui.screens.home.HomeScreen
15 | import com.mr3y.ludi.shared.ui.screens.onboarding.OnboardingScreen
16 | import com.mr3y.ludi.shared.ui.theme.LudiTheme
17 |
18 | @OptIn(ExperimentalMaterial3WindowSizeClassApi::class, ExperimentalCoilApi::class)
19 | @Composable
20 | fun App(
21 | isDarkTheme: Boolean,
22 | useDynamicColor: Boolean,
23 | showOnboardingScreen: Boolean,
24 | modifier: Modifier = Modifier
25 | ) {
26 | LudiTheme(
27 | darkTheme = isDarkTheme,
28 | dynamicColor = useDynamicColor
29 | ) {
30 | setSingletonImageLoaderFactory { context ->
31 | ImageLoader.Builder(context).build()
32 | }
33 | CompositionLocalProvider(
34 | LocalWindowSizeClass provides calculateWindowSizeClass()
35 | ) {
36 | if (showOnboardingScreen) {
37 | Navigator(screen = OnboardingScreen)
38 | } else {
39 | HomeScreen(modifier = modifier.fillMaxSize())
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/CrashReporting.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core
2 |
3 | interface CrashReporting {
4 |
5 | fun recordException(throwable: Throwable, logMessage: String?)
6 | }
7 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core
2 |
3 | interface Logger {
4 |
5 | fun d(throwable: Throwable? = null, tag: String = "", message: () -> String)
6 |
7 | fun i(throwable: Throwable? = null, tag: String = "", message: () -> String)
8 |
9 | fun e(throwable: Throwable? = null, tag: String = "", message: () -> String)
10 |
11 | fun v(throwable: Throwable? = null, tag: String = "", message: () -> String)
12 |
13 | fun w(throwable: Throwable? = null, tag: String = "", message: () -> String)
14 | }
15 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/database/Adapters.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.database
2 |
3 | import app.cash.sqldelight.ColumnAdapter
4 | import com.mr3y.ludi.shared.core.model.MarkupText
5 | import com.mr3y.ludi.shared.core.model.Title
6 | import java.time.ZonedDateTime
7 |
8 | object TitleColumnAdapter : ColumnAdapter {
9 |
10 | override fun encode(value: Title): String {
11 | return when (value) {
12 | is Title.Plain -> "Plain(${value.text})"
13 | is Title.Markup -> "Markup(${value.text})"
14 | }
15 | }
16 |
17 | override fun decode(databaseValue: String): Title {
18 | return when {
19 | databaseValue.startsWith(prefix = "Plain") -> {
20 | Title.Plain(databaseValue.removePrefix("Plain(").removeSuffix(")"))
21 | }
22 | databaseValue.startsWith(prefix = "Markup") -> {
23 | Title.Markup(databaseValue.removePrefix("Markup(").removeSuffix(")"))
24 | }
25 | else -> throw IllegalArgumentException("Unknown Title type discriminator value, Can't decode this title value: $databaseValue")
26 | }
27 | }
28 | }
29 |
30 | object MarkupTextColumnAdapter : ColumnAdapter {
31 | override fun encode(value: MarkupText): String = value.text
32 |
33 | override fun decode(databaseValue: String): MarkupText = MarkupText(databaseValue)
34 | }
35 |
36 | object ZonedDateTimeAdapter : ColumnAdapter {
37 | override fun encode(value: ZonedDateTime): String = value.toString()
38 |
39 | override fun decode(databaseValue: String): ZonedDateTime = ZonedDateTime.parse(databaseValue)
40 | }
41 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/database/DatabaseFactory.kt:
--------------------------------------------------------------------------------
1 | // ktlint-disable filename
2 | package com.mr3y.ludi.shared.core.database
3 |
4 | import app.cash.sqldelight.EnumColumnAdapter
5 | import app.cash.sqldelight.db.SqlDriver
6 | import com.mr3y.ludi.shared.ArticleEntity
7 |
8 | expect class DriverFactory {
9 | fun createDriver(): SqlDriver
10 | }
11 |
12 | fun createDatabase(driver: DriverFactory): LudiDatabase {
13 | return LudiDatabase(
14 | driver = driver.createDriver(),
15 | articleEntityAdapter = ArticleEntity.Adapter(
16 | titleAdapter = TitleColumnAdapter,
17 | descriptionAdapter = MarkupTextColumnAdapter,
18 | sourceAdapter = EnumColumnAdapter(),
19 | contentAdapter = MarkupTextColumnAdapter,
20 | publicationDateAdapter = ZonedDateTimeAdapter
21 | )
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/database/Wrapper.kt:
--------------------------------------------------------------------------------
1 | // ktlint-disable filename
2 | package com.mr3y.ludi.shared.core.database
3 |
4 | // kotlin-inject fails to resolve types generated by sqlDelight,
5 | // As a workaround we have to wrap those types to make them resolvable.
6 | // See: https://github.com/evant/kotlin-inject/issues/316
7 |
8 | @JvmInline
9 | value class LudiDatabaseWrapper(val value: LudiDatabase)
10 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/internal/KermitLogger.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.internal
2 |
3 | import com.mr3y.ludi.shared.core.Logger
4 | import me.tatarka.inject.annotations.Inject
5 | import co.touchlab.kermit.Logger as DelegatingLogger
6 |
7 | @Inject
8 | class KermitLogger(
9 | private val delegatingLogger: DelegatingLogger
10 | ) : Logger {
11 |
12 | override fun d(throwable: Throwable?, tag: String, message: () -> String) {
13 | delegatingLogger.d(throwable, tag, message)
14 | }
15 |
16 | override fun i(throwable: Throwable?, tag: String, message: () -> String) {
17 | delegatingLogger.i(throwable, tag, message)
18 | }
19 |
20 | override fun e(throwable: Throwable?, tag: String, message: () -> String) {
21 | delegatingLogger.e(throwable, tag, message)
22 | }
23 |
24 | override fun v(throwable: Throwable?, tag: String, message: () -> String) {
25 | delegatingLogger.v(throwable, tag, message)
26 | }
27 |
28 | override fun w(throwable: Throwable?, tag: String, message: () -> String) {
29 | delegatingLogger.w(throwable, tag, message)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/model/Article.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.model
2 |
3 | import java.time.ZonedDateTime
4 |
5 | sealed interface Article {
6 | val title: Title
7 | val description: MarkupText?
8 | val imageUrl: String?
9 | val content: MarkupText?
10 | val author: String?
11 | val source: Source
12 | val sourceLinkUrl: String
13 | val publicationDate: ZonedDateTime?
14 | }
15 |
16 | sealed interface Title {
17 | val text: String
18 | data class Markup(override val text: String) : Title
19 | data class Plain(override val text: String) : Title
20 | }
21 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/model/Deal.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.model
2 |
3 | import java.time.ZonedDateTime
4 |
5 | data class Deal(
6 | val internalName: String,
7 | val name: String,
8 | val metacriticUrl: String?,
9 | val dealID: String,
10 | val storeID: Int,
11 | val gameID: Long,
12 | val salePriceInUsDollar: Float,
13 | val normalPriceInUsDollar: Float,
14 | val isOnSale: Boolean,
15 | val savings: Double,
16 | val metacriticScore: Int,
17 | val steamRatingText: String?,
18 | val steamRatingPercent: Int,
19 | val steamRatingCount: Long,
20 | val steamAppID: Long?,
21 | val releaseDate: ZonedDateTime,
22 | val lastUpdate: ZonedDateTime,
23 | val dealRating: Float,
24 | val thumbnailUrl: String
25 | )
26 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/model/Game.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.model
2 |
3 | import java.time.ZonedDateTime
4 |
5 | data class GamesPage(
6 | val count: Long,
7 | val nextPageUrl: String?,
8 | val previousPageUrl: String?,
9 | val games: List
10 | )
11 |
12 | data class GamesGenresPage(
13 | val count: Long,
14 | val nextPageUrl: String?,
15 | val previousPageUrl: String?,
16 | val genres: Set
17 | )
18 |
19 | data class Game(
20 | val id: Long,
21 | val slug: String?,
22 | val name: String,
23 | val releaseDate: ZonedDateTime?,
24 | val toBeAnnounced: Boolean?,
25 | val imageUrl: String,
26 | val rating: Float,
27 | val metaCriticScore: Int?,
28 | val playtime: Int?,
29 | val suggestionsCount: Int,
30 | val platformsInfo: List?,
31 | val storesInfo: List?,
32 | val tags: List,
33 | val screenshots: List,
34 | val genres: List
35 | )
36 |
37 | data class PlatformInfo(
38 | val id: Int,
39 | val name: String,
40 | val slug: String,
41 | val imageUrl: String?,
42 | val yearEnd: Int?,
43 | val yearStart: Int?,
44 | val gamesCount: Long?,
45 | val imageBackground: String?,
46 | val releaseDate: ZonedDateTime?,
47 | val gameRequirementsInEnglish: GameRequirements?
48 | )
49 |
50 | data class GameRequirements(
51 | val minimum: MarkupText,
52 | val recommended: MarkupText
53 | )
54 |
55 | data class StoreInfo(
56 | val id: Int,
57 | val gameIdOnStore: Long?,
58 | val name: String,
59 | val slug: String?,
60 | val domain: String?,
61 | val gamesCount: Long?,
62 | val imageUrl: String?
63 | )
64 |
65 | data class GameTag(
66 | val id: Int,
67 | val name: String,
68 | val slug: String?,
69 | val language: String,
70 | val gamesCount: Long?,
71 | val imageUrl: String
72 | )
73 |
74 | data class GameScreenshot(val id: Long, val imageUrl: String)
75 |
76 | data class GameGenre(
77 | val id: Int,
78 | val name: String,
79 | val slug: String?,
80 | val gamesCount: Long?,
81 | val imageUrl: String?
82 | )
83 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/model/GiveawayEntry.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.model
2 |
3 | import java.time.ZonedDateTime
4 |
5 | @JvmInline
6 | value class Percent(val value: Int) {
7 | init {
8 | require(value in 0..100) { "Invalid Percentage value $value" }
9 | }
10 | }
11 |
12 | data class GiveawayEntry(
13 | val id: Long,
14 | val title: String,
15 | val worthInUsDollar: Float?,
16 | val thumbnailUrl: String,
17 | val imageUrl: String,
18 | val description: String,
19 | val instructions: String,
20 | val giveawayUrl: String,
21 | val publishedDateTime: ZonedDateTime?,
22 | val type: String,
23 | val platforms: List,
24 | val endDateTime: ZonedDateTime?,
25 | val users: Int,
26 | val status: GiveawayEntryStatus,
27 | val gamerPowerUrl: String
28 | )
29 |
30 | enum class GiveawayEntryStatus { Active }
31 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/model/MarkupText.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.model
2 |
3 | @JvmInline
4 | value class MarkupText(val text: String)
5 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/model/NewReleaseArticle.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.model
2 |
3 | import java.time.ZonedDateTime
4 |
5 | data class NewReleaseArticle(
6 | override val title: Title,
7 | override val description: MarkupText?,
8 | override val source: Source,
9 | override val sourceLinkUrl: String,
10 | val releaseDate: ZonedDateTime
11 | ) : Article {
12 | override val imageUrl: String? = null
13 | override val content: MarkupText? = null
14 | override val author: String? = null
15 | override val publicationDate: ZonedDateTime = releaseDate
16 | }
17 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/model/NewsArticle.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.model
2 |
3 | import java.time.ZonedDateTime
4 |
5 | data class NewsArticle(
6 | override val title: Title,
7 | override val description: MarkupText?,
8 | override val source: Source,
9 | override val sourceLinkUrl: String,
10 | override val content: MarkupText?,
11 | override val imageUrl: String?,
12 | override val author: String?,
13 | override val publicationDate: ZonedDateTime?
14 | ) : Article {
15 |
16 | override fun toString(): String {
17 | return "NewsArticle(title=\"$title\"," +
18 | " description=\"$description\",\n" +
19 | " source=\"$source\",\n" +
20 | " sourceLinkUrl=\"$sourceLinkUrl\",\n" +
21 | " content=\"$content\",\n" +
22 | " imageUrl=\"$imageUrl\",\n" +
23 | " author=\"$author\",\n" +
24 | " publicationDate=\"$publicationDate\")"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/model/Result.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.model
2 |
3 | import com.mr3y.ludi.shared.core.network.model.ApiResult
4 |
5 | sealed interface Result {
6 | data object Loading : Result
7 | data class Success(val data: T) : Result
8 | data class Error(val exception: Throwable? = null) : Result
9 | }
10 |
11 | /**
12 | * Allow transforming [Result.Success.data] if [Result] is [Result.Success] or return whatever result is otherwise.
13 | */
14 | inline fun Result.onSuccess(transform: (T) -> R): Result {
15 | return when (this) {
16 | is Result.Loading -> this
17 | is Result.Success -> Result.Success(transform(data))
18 | is Result.Error -> this
19 | }
20 | }
21 |
22 | val Result.data: R?
23 | get() = (this as? Result.Success)?.data
24 |
25 | val Result.exception: Throwable?
26 | get() = (this as? Result.Error)?.exception
27 |
28 | fun ApiResult.Error.toCoreErrorResult(): Result.Error {
29 | return Result.Error(throwable)
30 | }
31 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/model/ReviewArticle.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.model
2 |
3 | import java.time.ZonedDateTime
4 |
5 | data class ReviewArticle(
6 | override val title: Title,
7 | override val description: MarkupText?,
8 | override val imageUrl: String?,
9 | override val source: Source,
10 | override val content: MarkupText?,
11 | override val sourceLinkUrl: String,
12 | override val author: String?,
13 | override val publicationDate: ZonedDateTime?
14 | ) : Article
15 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/model/Source.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.model
2 |
3 | import ludi.shared.generated.resources.Res
4 | import ludi.shared.generated.resources.brutalgamer_logo
5 | import ludi.shared.generated.resources.eurogamer_logo
6 | import ludi.shared.generated.resources.game_spot_logo
7 | import ludi.shared.generated.resources.gamerant_logo
8 | import ludi.shared.generated.resources.giant_bomb_logo
9 | import ludi.shared.generated.resources.gloriousgaming_logo
10 | import ludi.shared.generated.resources.ign_logo
11 | import ludi.shared.generated.resources.pcgamer_logo
12 | import ludi.shared.generated.resources.pcgamesn_logo
13 | import ludi.shared.generated.resources.pcinvasion_logo
14 | import ludi.shared.generated.resources.polygon_logo
15 | import ludi.shared.generated.resources.rockpapershotgun_logo
16 | import ludi.shared.generated.resources.tech_radar_logo
17 | import ludi.shared.generated.resources.thegamer_logo
18 | import ludi.shared.generated.resources.venturebeat_logo
19 | import ludi.shared.generated.resources.vg247_logo
20 | import org.jetbrains.compose.resources.DrawableResource
21 |
22 | enum class Source {
23 | GiantBomb,
24 | GameSpot,
25 | IGN,
26 | TechRadar,
27 | PCGamesN,
28 | RockPaperShotgun,
29 | PCInvasion,
30 | GloriousGaming,
31 | EuroGamer,
32 | VG247,
33 | TheGamer,
34 | GameRant,
35 | BrutalGamer,
36 | VentureBeat,
37 | Polygon,
38 | PCGamer
39 | }
40 |
41 | internal fun mapTypeToIconRes(type: Source): DrawableResource {
42 | return when (type) {
43 | Source.GiantBomb -> Res.drawable.giant_bomb_logo
44 | Source.GameSpot -> Res.drawable.game_spot_logo
45 | Source.IGN -> Res.drawable.ign_logo
46 | Source.TechRadar -> Res.drawable.tech_radar_logo
47 | Source.PCGamesN -> Res.drawable.pcgamesn_logo
48 | Source.RockPaperShotgun -> Res.drawable.rockpapershotgun_logo
49 | Source.PCInvasion -> Res.drawable.pcinvasion_logo
50 | Source.GloriousGaming -> Res.drawable.gloriousgaming_logo
51 | Source.EuroGamer -> Res.drawable.eurogamer_logo
52 | Source.VG247 -> Res.drawable.vg247_logo
53 | Source.TheGamer -> Res.drawable.thegamer_logo
54 | Source.GameRant -> Res.drawable.gamerant_logo
55 | Source.BrutalGamer -> Res.drawable.brutalgamer_logo
56 | Source.VentureBeat -> Res.drawable.venturebeat_logo
57 | Source.Polygon -> Res.drawable.polygon_logo
58 | Source.PCGamer -> Res.drawable.pcgamer_logo
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/network/datasources/RSSFeedDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.network.datasources
2 |
3 | import com.mr3y.ludi.shared.core.model.NewReleaseArticle
4 | import com.mr3y.ludi.shared.core.model.NewsArticle
5 | import com.mr3y.ludi.shared.core.model.Result
6 | import com.mr3y.ludi.shared.core.model.ReviewArticle
7 | import com.mr3y.ludi.shared.core.model.Source
8 |
9 | interface RSSFeedDataSource {
10 |
11 | suspend fun fetchNewsFeed(source: Source): Result, Throwable>?
12 |
13 | suspend fun fetchReviewsFeed(source: Source): Result, Throwable>?
14 |
15 | suspend fun fetchNewReleasesFeed(source: Source): Result, Throwable>?
16 | }
17 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/network/datasources/internal/CheapSharkDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.network.datasources.internal
2 |
3 | import com.mr3y.ludi.shared.core.network.model.ApiResult
4 | import com.mr3y.ludi.shared.core.network.model.CheapSharkResponse
5 | import com.mr3y.ludi.shared.core.repository.query.DealsQueryParameters
6 | import com.mr3y.ludi.shared.core.repository.query.DealsSortingDirection
7 | import io.ktor.client.HttpClient
8 | import io.ktor.client.call.body
9 | import io.ktor.client.request.get
10 | import io.ktor.client.request.parameter
11 | import io.ktor.http.isSuccess
12 | import me.tatarka.inject.annotations.Inject
13 |
14 | interface CheapSharkDataSource {
15 |
16 | suspend fun queryLatestDeals(queryParameters: DealsQueryParameters, page: Int = 0, pageSize: Int = 60): ApiResult
17 | }
18 |
19 | @Inject
20 | class CheapSharkDataSourceImpl(
21 | private val client: HttpClient
22 | ) : CheapSharkDataSource {
23 | override suspend fun queryLatestDeals(queryParameters: DealsQueryParameters, page: Int, pageSize: Int): ApiResult {
24 | return try {
25 | val response = client.get("https://www.cheapshark.com/api/1.0/deals") {
26 | parameter("pageNumber", page)
27 | parameter("pageSize", pageSize)
28 | parameter("title", queryParameters.searchQuery)
29 | parameter("exact", queryParameters.matchTermsExactly?.let { if (it) 1 else 0 })
30 | parameter("storeID", queryParameters.stores?.joinToString(separator = ","))
31 | parameter("sortBy", queryParameters.sorting?.value)
32 | parameter("desc", queryParameters.sortingDirection?.let { if (it == DealsSortingDirection.Descending) 1 else 0 })
33 | }
34 | if (response.status.isSuccess()) {
35 | ApiResult.Success(
36 | CheapSharkResponse(totalPageCount = response.headers["x-total-page-count"]!!.toInt(), deals = response.body())
37 | )
38 | } else {
39 | ApiResult.Error(code = response.status.value)
40 | }
41 | } catch (e: Exception) {
42 | ApiResult.Error(code = null, throwable = e)
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/network/model/ApiResult.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.network.model
2 |
3 | sealed interface ApiResult {
4 | data class Success(val data: T) : ApiResult
5 | data class Error(val code: Int?, val throwable: Throwable? = null) : ApiResult
6 | }
7 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/network/model/GamerPowerGiveaway.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.network.model
2 |
3 | import com.mr3y.ludi.shared.core.model.GiveawayEntry
4 | import com.mr3y.ludi.shared.core.model.GiveawayEntryStatus
5 | import com.mr3y.ludi.shared.core.network.serialization.NotAvailableAsNullSerializer
6 | import kotlinx.serialization.SerialName
7 | import kotlinx.serialization.Serializable
8 |
9 | @Serializable
10 | data class GamerPowerGiveawayEntry(
11 | val id: Long,
12 | val title: String,
13 | @Serializable(with = NotAvailableAsNullSerializer::class)
14 | val worth: String?,
15 | @SerialName("thumbnail")
16 | val thumbnailUrl: String,
17 | @SerialName("image")
18 | val imageUrl: String,
19 | val description: String,
20 | val instructions: String,
21 | @SerialName("open_giveaway_url")
22 | val giveawayUrl: String,
23 | @SerialName("published_date")
24 | val publishedDateTime: String,
25 | val type: String,
26 | val platforms: String,
27 | @Serializable(with = NotAvailableAsNullSerializer::class)
28 | @SerialName("end_date")
29 | val endDateTime: String?,
30 | val users: Int,
31 | val status: GamerPowerGiveawayEntryStatus,
32 | @SerialName("gamerpower_url")
33 | val gamerPowerUrl: String
34 | )
35 |
36 | enum class GamerPowerGiveawayEntryStatus { Active }
37 |
38 | fun GamerPowerGiveawayEntry.toGiveawayEntry(): GiveawayEntry {
39 | return GiveawayEntry(
40 | id = id,
41 | title = title,
42 | worthInUsDollar = worth?.substringAfter('$')?.toFloat(),
43 | thumbnailUrl = thumbnailUrl,
44 | imageUrl = imageUrl,
45 | description = description,
46 | instructions = instructions,
47 | giveawayUrl = giveawayUrl,
48 | publishedDateTime = publishedDateTime.toZonedDateTime(pattern = Pattern.ISO_UTC_DATE_TIME),
49 | type = type,
50 | platforms = platforms.split(',').map { it.trim() },
51 | endDateTime = endDateTime?.toZonedDateTime(pattern = Pattern.ISO_UTC_DATE_TIME),
52 | users = users,
53 | status = GiveawayEntryStatus.Active,
54 | gamerPowerUrl = gamerPowerUrl
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/network/model/TimeConversionsUtils.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.network.model
2 |
3 | import java.text.ParsePosition
4 | import java.time.Instant
5 | import java.time.ZoneId
6 | import java.time.ZonedDateTime
7 | import java.time.format.DateTimeFormatter
8 | import java.time.format.DateTimeParseException
9 |
10 | internal fun Long.convertEpochSecondToZonedDateTime(): ZonedDateTime {
11 | return ZonedDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneId.systemDefault())
12 | }
13 |
14 | /**
15 | * Converts the following date format: "yyyy-mm-dd" to ZonedDateTime
16 | */
17 | internal fun String.toZonedDate(): ZonedDateTime {
18 | val (year, month, dayOfMonth) = split('-').map { it.toInt() }
19 | return ZonedDateTime.of(year, month, dayOfMonth, 0, 0, 0, 0, ZoneId.systemDefault())
20 | }
21 |
22 | internal fun String.toZonedDateTime(pattern: Pattern = Pattern.RFC_1123): ZonedDateTime? {
23 | return if (DateTimeFormatter.ISO_ZONED_DATE_TIME.parseUnresolved(this, ParsePosition(0)) != null) {
24 | ZonedDateTime.parse(this)
25 | } else {
26 | try {
27 | when (pattern) {
28 | Pattern.RFC_1123 -> ZonedDateTime.parse(this, DateTimeFormatter.RFC_1123_DATE_TIME)
29 | Pattern.ISO_UTC_DATE_TIME -> ZonedDateTime.parse("${this.replace(' ', 'T').removeSuffix("Z")}Z")
30 | }
31 | } catch (e: DateTimeParseException) {
32 | null
33 | }
34 | }
35 | }
36 |
37 | internal enum class Pattern {
38 | /**
39 | * A date time format pattern for date time strings like: "Thu, 02 Mar 2023 03:00:00 -0800", "Thu, 2 Mar 2023 01:30:02 +0000"
40 | */
41 | RFC_1123,
42 |
43 | /**
44 | * A date time format pattern for date time strings like: "2023-03-24T15:16:31Z", "2023-03-24 15:16:31"
45 | */
46 | ISO_UTC_DATE_TIME
47 | }
48 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/network/rssparser/Parser.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.network.rssparser
2 |
3 | import com.mr3y.ludi.shared.core.model.NewReleaseArticle
4 | import com.mr3y.ludi.shared.core.model.NewsArticle
5 | import com.mr3y.ludi.shared.core.model.ReviewArticle
6 | import com.mr3y.ludi.shared.core.model.Source
7 |
8 | interface Parser {
9 |
10 | suspend fun parseNewsArticlesAtUrl(url: String, source: Source): List
11 |
12 | suspend fun parseReviewArticlesAtUrl(url: String, source: Source): List
13 |
14 | suspend fun parseNewReleaseArticlesAtUrl(url: String, source: Source): List
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/network/serialization/GamerPowerModelsSerializers.kt:
--------------------------------------------------------------------------------
1 | // ktlint-disable filename
2 | package com.mr3y.ludi.shared.core.network.serialization
3 |
4 | import kotlinx.serialization.KSerializer
5 | import kotlinx.serialization.descriptors.PrimitiveKind
6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
7 | import kotlinx.serialization.descriptors.SerialDescriptor
8 | import kotlinx.serialization.encoding.Decoder
9 | import kotlinx.serialization.encoding.Encoder
10 |
11 | object NotAvailableAsNullSerializer : KSerializer {
12 | override val descriptor: SerialDescriptor
13 | get() = PrimitiveSerialDescriptor(serialName = "String?", kind = PrimitiveKind.STRING)
14 |
15 | override fun deserialize(decoder: Decoder): String? {
16 | return when (val value = decoder.decodeString()) {
17 | "N/A" -> null
18 | else -> value
19 | }
20 | }
21 |
22 | override fun serialize(encoder: Encoder, value: String?) {
23 | encoder.encodeString(value?.toString() ?: "N/A")
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/network/serialization/RAWGModelsSerializers.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.network.serialization
2 |
3 | import com.mr3y.ludi.shared.core.network.model.DetailedRAWGPlatformInfo
4 | import com.mr3y.ludi.shared.core.network.model.RAWGPlatformInfo
5 | import com.mr3y.ludi.shared.core.network.model.RAWGStoreInfo
6 | import com.mr3y.ludi.shared.core.network.model.ShallowRAWGPlatformInfo
7 | import com.mr3y.ludi.shared.core.network.model.ShallowRAWGStoreInfo
8 | import com.mr3y.ludi.shared.core.network.model.ShallowRAWGStoreInfoWithId
9 | import kotlinx.serialization.DeserializationStrategy
10 | import kotlinx.serialization.json.JsonContentPolymorphicSerializer
11 | import kotlinx.serialization.json.JsonElement
12 | import kotlinx.serialization.json.jsonObject
13 |
14 | object RAWGPlatformSerializer : JsonContentPolymorphicSerializer(RAWGPlatformInfo::class) {
15 | override fun selectDeserializer(element: JsonElement): DeserializationStrategy {
16 | return when {
17 | "requirements_en" in element.jsonObject || "released_at" in element.jsonObject -> DetailedRAWGPlatformInfo.serializer()
18 | else -> ShallowRAWGPlatformInfo.serializer()
19 | }
20 | }
21 | }
22 |
23 | object RAWGStoreSerializer : JsonContentPolymorphicSerializer(RAWGStoreInfo::class) {
24 | override fun selectDeserializer(element: JsonElement): DeserializationStrategy {
25 | return when {
26 | "id" in element.jsonObject -> ShallowRAWGStoreInfoWithId.serializer()
27 | else -> ShallowRAWGStoreInfo.serializer()
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/paging/DealsPagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.paging
2 |
3 | import androidx.paging.PagingState
4 | import app.cash.paging.PagingSource
5 | import com.mr3y.ludi.shared.core.CrashReporting
6 | import com.mr3y.ludi.shared.core.model.Deal
7 | import com.mr3y.ludi.shared.core.network.datasources.internal.CheapSharkDataSource
8 | import com.mr3y.ludi.shared.core.network.model.ApiResult
9 | import com.mr3y.ludi.shared.core.network.model.toDeal
10 | import com.mr3y.ludi.shared.core.repository.query.DealsQueryParameters
11 |
12 | class DealsPagingSource(
13 | private val networkDataSource: CheapSharkDataSource,
14 | private val query: DealsQueryParameters,
15 | private val crashReporting: CrashReporting
16 | ) : PagingSource() {
17 |
18 | override fun getRefreshKey(state: PagingState): Int? {
19 | return state.anchorPosition?.let { anchorPosition ->
20 | val anchorPage = state.closestPageToPosition(anchorPosition)
21 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
22 | }
23 | }
24 |
25 | override suspend fun load(params: LoadParams): LoadResult {
26 | val pageNum = params.key?.coerceAtLeast(0) ?: 0
27 | return when (val response = networkDataSource.queryLatestDeals(query, pageNum, params.loadSize)) {
28 | is ApiResult.Success -> {
29 | LoadResult.Page(
30 | data = response.data.deals.map { it.toDeal() },
31 | prevKey = if (pageNum > 0) pageNum - 1 else null,
32 | nextKey = if (pageNum < response.data.totalPageCount) pageNum + 1 else null
33 | )
34 | }
35 | is ApiResult.Error -> {
36 | if (response.throwable != null) {
37 | crashReporting.recordException(response.throwable, logMessage = "Error occurred while querying deals with query $query")
38 | return LoadResult.Error(response.throwable)
39 | }
40 | return LoadResult.Invalid()
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/paging/RAWGGamesPagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.paging
2 |
3 | import app.cash.paging.PagingSource
4 | import app.cash.paging.PagingState
5 | import com.mr3y.ludi.shared.core.CrashReporting
6 | import com.mr3y.ludi.shared.core.model.Game
7 | import com.mr3y.ludi.shared.core.network.datasources.internal.RAWGDataSource
8 | import com.mr3y.ludi.shared.core.network.model.ApiResult
9 | import com.mr3y.ludi.shared.core.network.model.toGame
10 | import com.mr3y.ludi.shared.core.repository.query.GamesQueryParameters
11 |
12 | class RAWGGamesPagingSource(
13 | private val networkDataSource: RAWGDataSource,
14 | private val query: GamesQueryParameters,
15 | private val crashReporting: CrashReporting
16 | ) : PagingSource() {
17 |
18 | override fun getRefreshKey(state: PagingState): Int? {
19 | return state.anchorPosition?.let { anchorPosition ->
20 | val anchorPage = state.closestPageToPosition(anchorPosition)
21 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
22 | }
23 | }
24 |
25 | override suspend fun load(params: LoadParams): LoadResult {
26 | val pageNum = params.key?.coerceAtLeast(1) ?: 1
27 | return when (val response = networkDataSource.queryGames(query, pageNum, params.loadSize)) {
28 | is ApiResult.Success -> {
29 | LoadResult.Page(
30 | data = response.data.results.mapNotNull { it.toGame() },
31 | prevKey = if (response.data.previousPageUrl != null) pageNum - 1 else null,
32 | nextKey = if (response.data.nextPageUrl != null) pageNum + 1 else null
33 | )
34 | }
35 | is ApiResult.Error -> {
36 | if (response.throwable != null) {
37 | crashReporting.recordException(response.throwable, logMessage = "Error occurred while querying RAWG Games with query $query")
38 | return LoadResult.Error(response.throwable)
39 | }
40 | return LoadResult.Invalid()
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/repository/DealsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.repository
2 |
3 | import app.cash.paging.PagingData
4 | import com.mr3y.ludi.shared.core.model.Deal
5 | import com.mr3y.ludi.shared.core.model.GiveawayEntry
6 | import com.mr3y.ludi.shared.core.model.Result
7 | import com.mr3y.ludi.shared.core.repository.query.DealsQueryParameters
8 | import com.mr3y.ludi.shared.core.repository.query.GiveawaysQueryParameters
9 | import kotlinx.coroutines.flow.Flow
10 |
11 | interface DealsRepository {
12 |
13 | fun queryDeals(queryParameters: DealsQueryParameters): Flow>
14 |
15 | suspend fun queryGiveaways(queryParameters: GiveawaysQueryParameters): Result, Throwable>
16 | }
17 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/repository/GamesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.repository
2 |
3 | import app.cash.paging.PagingData
4 | import com.mr3y.ludi.shared.core.model.Game
5 | import com.mr3y.ludi.shared.core.model.GamesGenresPage
6 | import com.mr3y.ludi.shared.core.model.Result
7 | import com.mr3y.ludi.shared.core.repository.query.GamesQueryParameters
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | interface GamesRepository {
11 | fun queryGames(queryParameters: GamesQueryParameters): Flow>
12 |
13 | suspend fun queryGamesGenres(): Result
14 | }
15 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/repository/NewsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.repository
2 |
3 | import app.cash.paging.PagingData
4 | import com.mr3y.ludi.shared.core.model.NewReleaseArticle
5 | import com.mr3y.ludi.shared.core.model.NewsArticle
6 | import com.mr3y.ludi.shared.core.model.ReviewArticle
7 | import com.mr3y.ludi.shared.core.model.Source
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | interface NewsRepository {
11 |
12 | fun queryLatestGamingNews(searchQuery: String?): Flow>
13 |
14 | fun queryGamesNewReleases(): Flow>
15 |
16 | fun queryGamesReviews(searchQuery: String?): Flow>
17 |
18 | suspend fun updateGamingNews(sources: Set, forceRefresh: Boolean): Boolean
19 |
20 | suspend fun updateGamesNewReleases(sources: Set, forceRefresh: Boolean): Boolean
21 |
22 | suspend fun updateGamesReviews(sources: Set, forceRefresh: Boolean): Boolean
23 | }
24 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/repository/internal/DefaultGamesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.repository.internal
2 |
3 | import app.cash.paging.Pager
4 | import app.cash.paging.PagingConfig
5 | import app.cash.paging.PagingData
6 | import com.mr3y.ludi.shared.core.CrashReporting
7 | import com.mr3y.ludi.shared.core.model.Game
8 | import com.mr3y.ludi.shared.core.model.GamesGenresPage
9 | import com.mr3y.ludi.shared.core.model.Result
10 | import com.mr3y.ludi.shared.core.model.toCoreErrorResult
11 | import com.mr3y.ludi.shared.core.network.datasources.internal.RAWGDataSource
12 | import com.mr3y.ludi.shared.core.network.model.ApiResult
13 | import com.mr3y.ludi.shared.core.network.model.toGamesGenresPage
14 | import com.mr3y.ludi.shared.core.paging.RAWGGamesPagingSource
15 | import com.mr3y.ludi.shared.core.repository.GamesRepository
16 | import com.mr3y.ludi.shared.core.repository.query.GamesQueryParameters
17 | import kotlinx.coroutines.flow.Flow
18 | import me.tatarka.inject.annotations.Inject
19 |
20 | @Inject
21 | class DefaultGamesRepository(
22 | private val rawgDataSource: RAWGDataSource,
23 | private val crashReporting: CrashReporting
24 | ) : GamesRepository {
25 |
26 | override fun queryGames(queryParameters: GamesQueryParameters): Flow> {
27 | return Pager(DefaultPagingConfig) {
28 | RAWGGamesPagingSource(
29 | rawgDataSource,
30 | queryParameters,
31 | crashReporting
32 | )
33 | }.flow
34 | }
35 |
36 | override suspend fun queryGamesGenres(): Result {
37 | return when (val result = rawgDataSource.queryGamesGenres()) {
38 | is ApiResult.Success -> {
39 | Result.Success(result.data.toGamesGenresPage())
40 | }
41 | is ApiResult.Error -> {
42 | val errorResult = result.toCoreErrorResult()
43 | if (errorResult.exception != null) {
44 | crashReporting.recordException(errorResult.exception, logMessage = "Error occurred while querying RAWG game Genres")
45 | }
46 | errorResult
47 | }
48 | }
49 | }
50 |
51 | companion object {
52 | private val DefaultPagingConfig = PagingConfig(pageSize = 20, initialLoadSize = 20)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/repository/query/DealsQueryParameters.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.repository.query
2 |
3 | fun DealsQuery(
4 | searchQuery: String? = null,
5 | matchTermsExactly: Boolean? = null,
6 | stores: List? = null,
7 | sortingCriteria: DealsSorting? = null,
8 | sortingDirection: DealsSortingDirection? = null
9 | ) = DealsQueryParameters(searchQuery, matchTermsExactly, stores, sortingCriteria, sortingDirection)
10 |
11 | data class DealsQueryParameters(
12 | val searchQuery: String?,
13 | val matchTermsExactly: Boolean?,
14 | val stores: List?,
15 | val sorting: DealsSorting?,
16 | val sortingDirection: DealsSortingDirection?
17 | )
18 |
19 | enum class DealsSorting(val value: String) {
20 | DealRating("Deal Rating"),
21 | Title("Title"),
22 | Savings("Savings"),
23 | Price("Price"),
24 | Metacritic("Metacritic"),
25 | Reviews("Reviews"),
26 | Release("Release"),
27 | Store("Store"),
28 | Recent("Recent")
29 | }
30 |
31 | enum class DealsSortingDirection {
32 | Ascending,
33 | Descending
34 | }
35 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/repository/query/GamesQueryParameters.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.repository.query
2 |
3 | fun GamesQuery(
4 | searchQuery: String? = null,
5 | isFuzzinessEnabled: Boolean? = null,
6 | matchTermsExactly: Boolean? = null,
7 | parentPlatforms: List? = null,
8 | platforms: List? = null,
9 | stores: List? = null,
10 | developers: List? = null,
11 | genres: List? = null,
12 | tags: List? = null,
13 | dates: List? = null,
14 | metaCriticScores: List? = null,
15 | sortingCriteria: GamesSortingCriteria? = null
16 | ) = GamesQueryParameters(searchQuery, isFuzzinessEnabled, matchTermsExactly, parentPlatforms, platforms, stores, developers, genres, tags, dates, metaCriticScores, sortingCriteria)
17 |
18 | data class GamesQueryParameters(
19 | val searchQuery: String?,
20 | val isFuzzinessEnabled: Boolean?,
21 | val matchTermsExactly: Boolean?,
22 | val parentPlatforms: List?,
23 | val platforms: List?,
24 | val stores: List?,
25 | val developers: List?,
26 | val genres: List?,
27 | val tags: List?,
28 | val dates: List?,
29 | val metaCriticScores: List?,
30 | val sortingCriteria: GamesSortingCriteria?
31 | ) {
32 | companion object {
33 | val Empty = GamesQueryParameters(
34 | null,
35 | null,
36 | null,
37 | null,
38 | null,
39 | null,
40 | null,
41 | null,
42 | null,
43 | null,
44 | null,
45 | null
46 | )
47 | }
48 | }
49 |
50 | enum class GamesSortingCriteria(val apiName: String) {
51 | NameAscending("name"), // A-Z
52 | NameDescending("-name"), // Z-A
53 | ReleasedAscending("released"),
54 | ReleasedDescending("-released"),
55 | DateAddedAscending("added"),
56 | DateAddedDescending("-added"),
57 | DateCreatedAscending("created"),
58 | DateCreatedDescending("-created"),
59 | DateUpdatedAscending("updated"),
60 | DateUpdatedDescending("-updated"),
61 | RatingAscending("rating"),
62 | RatingDescending("-rating"),
63 | MetacriticScoreAscending("metacritic"),
64 | MetacriticScoreDescending("-metacritic")
65 | }
66 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/core/repository/query/GiveawaysQueryParameters.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.repository.query
2 |
3 | data class GiveawaysQueryParameters(
4 | val platforms: List?,
5 | val stores: List?,
6 | val sorting: GiveawaysSorting?
7 | )
8 |
9 | enum class GiveawayPlatform(val value: String) {
10 | PC("pc"),
11 | Playstation4("ps4"),
12 | Playstation5("ps5"),
13 | XboxOne("xbox-one"),
14 | XboxSeriesXs("xbox-series-xs"),
15 | Xbox360("xbox-360"),
16 | Android("android"),
17 | IOS("ios"),
18 | NintendoSwitch("switch")
19 | }
20 |
21 | enum class GiveawayStore(val value: String) {
22 | Steam("steam"),
23 | EpicGames("epic-games-store"),
24 | Ubisoft("ubisoft"),
25 | GOG("gog"),
26 | Itchio("itchio"),
27 | Origin("origin"),
28 | Battlenet("battlenet")
29 | }
30 |
31 | enum class GiveawaysSorting(val value: String) {
32 | Value("value"),
33 | Popularity("popularity"),
34 | Date("date")
35 | }
36 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/CoroutinesComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.Singleton
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.SupervisorJob
7 | import me.tatarka.inject.annotations.Provides
8 |
9 | @JvmInline
10 | value class ApplicationScope(val value: CoroutineScope)
11 |
12 | interface CoroutinesComponent {
13 |
14 | @Singleton
15 | @Provides
16 | fun providesApplicationCoroutineScope(): ApplicationScope {
17 | return ApplicationScope(CoroutineScope(SupervisorJob() + Dispatchers.IO))
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/DatabaseDispatcherComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 |
5 | expect interface DatabaseDispatcherComponent
6 |
7 | @JvmInline
8 | value class DatabaseDispatcher(val dispatcher: CoroutineDispatcher)
9 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/LoggingComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import co.touchlab.kermit.Logger
4 | import co.touchlab.kermit.loggerConfigInit
5 | import co.touchlab.kermit.platformLogWriter
6 | import com.mr3y.ludi.shared.core.internal.KermitLogger
7 | import com.mr3y.ludi.shared.di.annotations.Singleton
8 | import me.tatarka.inject.annotations.Provides
9 |
10 | interface LoggingComponent {
11 |
12 | @Singleton
13 | @Provides
14 | fun provideLoggerInstance(): Logger {
15 | return Logger(config = loggerConfigInit(platformLogWriter()))
16 | }
17 |
18 | @Singleton
19 | @Provides
20 | fun provideKermitLoggerInstance(kermitLogger: Logger): com.mr3y.ludi.shared.core.Logger {
21 | return KermitLogger(delegatingLogger = kermitLogger)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/RESTfulDataSourcesComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.core.network.datasources.internal.CheapSharkDataSource
4 | import com.mr3y.ludi.shared.core.network.datasources.internal.CheapSharkDataSourceImpl
5 | import com.mr3y.ludi.shared.core.network.datasources.internal.GamerPowerDataSource
6 | import com.mr3y.ludi.shared.core.network.datasources.internal.GamerPowerDataSourceImpl
7 | import com.mr3y.ludi.shared.core.network.datasources.internal.RAWGDataSource
8 | import com.mr3y.ludi.shared.core.network.datasources.internal.RAWGDataSourceImpl
9 | import com.mr3y.ludi.shared.di.annotations.Singleton
10 | import io.ktor.client.HttpClient
11 | import me.tatarka.inject.annotations.Provides
12 |
13 | interface RESTfulDataSourcesComponent {
14 |
15 | @Provides
16 | @Singleton
17 | fun provideRAWGDataSourceInstance(client: HttpClient): RAWGDataSource {
18 | return RAWGDataSourceImpl(client)
19 | }
20 |
21 | @Provides
22 | @Singleton
23 | fun provideCheapSharkDataSourceInstance(client: HttpClient): CheapSharkDataSource {
24 | return CheapSharkDataSourceImpl(client)
25 | }
26 |
27 | @Provides
28 | @Singleton
29 | fun provideGamerPowerDataSourceInstance(client: HttpClient): GamerPowerDataSource {
30 | return GamerPowerDataSourceImpl(client)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/RSSFeedDataSourcesComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.core.network.datasources.RSSFeedDataSource
4 | import com.mr3y.ludi.shared.core.network.datasources.internal.DefaultRSSFeedDataSource
5 | import com.mr3y.ludi.shared.di.annotations.Singleton
6 | import me.tatarka.inject.annotations.Provides
7 |
8 | interface RSSFeedDataSourcesComponent {
9 |
10 | @Provides
11 | @Singleton
12 | fun DefaultRSSFeedDataSource.bind(): RSSFeedDataSource = this
13 | }
14 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/RepositoriesComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.core.repository.DealsRepository
4 | import com.mr3y.ludi.shared.core.repository.GamesRepository
5 | import com.mr3y.ludi.shared.core.repository.NewsRepository
6 | import com.mr3y.ludi.shared.core.repository.internal.DefaultDealsRepository
7 | import com.mr3y.ludi.shared.core.repository.internal.DefaultGamesRepository
8 | import com.mr3y.ludi.shared.core.repository.internal.DefaultNewsRepository
9 | import com.mr3y.ludi.shared.di.annotations.Singleton
10 | import me.tatarka.inject.annotations.Provides
11 |
12 | interface RepositoriesComponent {
13 |
14 | @Provides
15 | @Singleton
16 | fun DefaultNewsRepository.bind(): NewsRepository = this
17 |
18 | @Provides
19 | @Singleton
20 | fun DefaultGamesRepository.bind(): GamesRepository = this
21 |
22 | @Provides
23 | @Singleton
24 | fun DefaultDealsRepository.bind(): DealsRepository = this
25 | }
26 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/SharedApplicationComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.LudiSharedState
4 |
5 | interface SharedApplicationComponent :
6 | CoroutinesComponent,
7 | DataStoreComponent,
8 | LoggingComponent,
9 | NetworkComponent,
10 | TimeSourceComponent,
11 | SharedDatabaseComponent,
12 | RepositoriesComponent,
13 | RESTfulDataSourcesComponent,
14 | RSSFeedDataSourcesComponent {
15 | val appState: LudiSharedState
16 | }
17 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/SharedDatabaseComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.core.database.DriverFactory
4 | import com.mr3y.ludi.shared.core.database.LudiDatabaseWrapper
5 | import com.mr3y.ludi.shared.core.database.createDatabase
6 | import com.mr3y.ludi.shared.core.database.dao.ArticleEntitiesDao
7 | import com.mr3y.ludi.shared.core.database.dao.DefaultArticleEntitiesDao
8 | import com.mr3y.ludi.shared.di.annotations.Singleton
9 | import me.tatarka.inject.annotations.Provides
10 |
11 | interface SharedDatabaseComponent : DatabaseDispatcherComponent {
12 |
13 | @Singleton
14 | @Provides
15 | fun provideDatabaseInstance(platformSqlDriverFactory: DriverFactory): LudiDatabaseWrapper {
16 | return LudiDatabaseWrapper(value = createDatabase(platformSqlDriverFactory))
17 | }
18 |
19 | @Singleton
20 | @Provides
21 | fun provideArticleEntitiesDaoInstance(database: LudiDatabaseWrapper, dispatcher: DatabaseDispatcher): ArticleEntitiesDao {
22 | return DefaultArticleEntitiesDao(database.value, dispatcher)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/SharedDealsFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import cafe.adriel.voyager.core.model.ScreenModel
4 | import com.mr3y.ludi.shared.di.annotations.DealsFeatureScope
5 | import com.mr3y.ludi.shared.ui.presenter.DealsViewModel
6 | import me.tatarka.inject.annotations.Provides
7 |
8 | interface SharedDealsFeatureComponent {
9 |
10 | val bind: ScreenModel
11 |
12 | @Provides
13 | @DealsFeatureScope
14 | fun DealsViewModel.provideDealsViewModelInstance(): ScreenModel = this
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/SharedDiscoverFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import cafe.adriel.voyager.core.model.ScreenModel
4 | import com.mr3y.ludi.shared.di.annotations.DiscoverFeatureScope
5 | import com.mr3y.ludi.shared.ui.presenter.DefaultDiscoverPagingFactory
6 | import com.mr3y.ludi.shared.ui.presenter.DiscoverPagingFactory
7 | import com.mr3y.ludi.shared.ui.presenter.DiscoverViewModel
8 | import me.tatarka.inject.annotations.Provides
9 |
10 | interface SharedDiscoverFeatureComponent {
11 |
12 | val bind: ScreenModel
13 |
14 | @Provides
15 | @DiscoverFeatureScope
16 | fun DiscoverViewModel.provideDiscoverViewModelInstance(): ScreenModel = this
17 |
18 | @Provides
19 | @DiscoverFeatureScope
20 | fun DefaultDiscoverPagingFactory.bind(): DiscoverPagingFactory = this
21 | }
22 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/SharedNewsFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import cafe.adriel.voyager.core.model.ScreenModel
4 | import com.mr3y.ludi.shared.core.repository.NewsRepository
5 | import com.mr3y.ludi.shared.di.annotations.NewsFeatureScope
6 | import com.mr3y.ludi.shared.ui.datastore.FollowedNewsDataSourcesDataStore
7 | import com.mr3y.ludi.shared.ui.presenter.NewsFeedThrottler
8 | import com.mr3y.ludi.shared.ui.presenter.NewsViewModel
9 | import me.tatarka.inject.annotations.Provides
10 |
11 | interface SharedNewsFeatureComponent {
12 |
13 | val bind: ScreenModel
14 |
15 | @Provides
16 | @NewsFeatureScope
17 | fun provideNewsViewModelInstance(
18 | newsRepository: NewsRepository,
19 | followedNewsDataSourcesDataStore: FollowedNewsDataSourcesDataStore,
20 | throttler: NewsFeedThrottler
21 | ): ScreenModel {
22 | return NewsViewModel(newsRepository, followedNewsDataSourcesDataStore.value, throttler)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/SharedOnboardingFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import cafe.adriel.voyager.core.model.ScreenModel
4 | import com.mr3y.ludi.shared.di.annotations.OnboardingFeatureScope
5 | import com.mr3y.ludi.shared.ui.presenter.OnBoardingViewModel
6 | import me.tatarka.inject.annotations.Provides
7 |
8 | interface SharedOnboardingFeatureComponent {
9 |
10 | val bind: ScreenModel
11 |
12 | @Provides
13 | @OnboardingFeatureScope
14 | fun OnBoardingViewModel.provideOnboardingViewModelInstance(): ScreenModel = this
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/SharedSettingsFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import cafe.adriel.voyager.core.model.ScreenModel
4 | import com.mr3y.ludi.shared.di.annotations.SettingsFeatureScope
5 | import com.mr3y.ludi.shared.ui.presenter.SettingsViewModel
6 | import me.tatarka.inject.annotations.Provides
7 |
8 | interface SharedSettingsFeatureComponent {
9 |
10 | val bind: ScreenModel
11 |
12 | @Provides
13 | @SettingsFeatureScope
14 | fun SettingsViewModel.provideSettingsViewModelInstance(): ScreenModel = this
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/TimeSourceComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | expect interface TimeSourceComponent
4 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/VoyagerIntegration.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import androidx.compose.runtime.Composable
4 | import cafe.adriel.voyager.core.model.ScreenModel
5 | import cafe.adriel.voyager.core.screen.Screen
6 |
7 | @Composable
8 | expect inline fun Screen.getScreenModel(tag: String? = null): T
9 |
10 | @Composable
11 | expect inline fun Screen.getScreenModel(tag: String? = null, arg1: P): T
12 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/annotations/DealsFeatureScope.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di.annotations
2 |
3 | import me.tatarka.inject.annotations.Scope
4 |
5 | @Scope
6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER)
7 | annotation class DealsFeatureScope
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/annotations/DiscoverFeatureScope.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di.annotations
2 |
3 | import me.tatarka.inject.annotations.Scope
4 |
5 | @Scope
6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER)
7 | annotation class DiscoverFeatureScope
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/annotations/NewsFeatureScope.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di.annotations
2 |
3 | import me.tatarka.inject.annotations.Scope
4 |
5 | @Scope
6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER)
7 | annotation class NewsFeatureScope
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/annotations/OnboardingFeatureScope.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di.annotations
2 |
3 | import me.tatarka.inject.annotations.Scope
4 |
5 | @Scope
6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER)
7 | annotation class OnboardingFeatureScope
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/annotations/SettingsFeatureScope.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di.annotations
2 |
3 | import me.tatarka.inject.annotations.Scope
4 |
5 | @Scope
6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER)
7 | annotation class SettingsFeatureScope
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/di/annotations/Singleton.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di.annotations
2 |
3 | import me.tatarka.inject.annotations.Scope
4 |
5 | @Scope
6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER)
7 | annotation class Singleton
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/adaptive/LocalWindowSizeClass.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.adaptive
2 |
3 | import androidx.compose.material3.windowsizeclass.WindowSizeClass
4 | import androidx.compose.runtime.staticCompositionLocalOf
5 |
6 | val LocalWindowSizeClass = staticCompositionLocalOf { error("No WindowSizeClass available") }
7 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/components/LudiAsyncImage.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Alignment
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.graphics.FilterQuality
7 | import androidx.compose.ui.graphics.ImageBitmap
8 | import androidx.compose.ui.graphics.painter.Painter
9 | import androidx.compose.ui.layout.ContentScale
10 | import coil3.Bitmap
11 | import coil3.compose.AsyncImagePainter
12 | import coil3.compose.LocalPlatformContext
13 | import coil3.request.ImageRequest
14 |
15 | @Composable
16 | fun LudiAsyncImage(
17 | url: String?,
18 | contentDescription: String?,
19 | modifier: Modifier = Modifier,
20 | placeholder: Painter? = null,
21 | error: Painter? = null,
22 | onState: ((AsyncImagePainter.State) -> Unit)? = null,
23 | contentScale: ContentScale = ContentScale.Fit,
24 | alignment: Alignment = Alignment.Center,
25 | customMemoryCacheKey: String? = null,
26 | allowBitmapHardware: Boolean = true,
27 | filterQuality: FilterQuality = FilterQuality.Low
28 | ) {
29 | val context = LocalPlatformContext.current
30 | coil3.compose.AsyncImage(
31 | model = ImageRequest.Builder(context)
32 | .data(url)
33 | .memoryCacheKey(customMemoryCacheKey)
34 | .platformSpecificConfig(allowBitmapHardware)
35 | .build(),
36 | contentDescription = contentDescription,
37 | transform = { state ->
38 | when (state) {
39 | is AsyncImagePainter.State.Loading -> AsyncImagePainter.State.Loading(painter = placeholder)
40 | is AsyncImagePainter.State.Error -> AsyncImagePainter.State.Error(painter = error, result = state.result)
41 | else -> state
42 | }
43 | },
44 | onState = onState,
45 | contentScale = contentScale,
46 | alignment = alignment,
47 | filterQuality = filterQuality,
48 | modifier = modifier
49 | )
50 | }
51 |
52 | expect fun ImageRequest.Builder.platformSpecificConfig(allowHardware: Boolean): ImageRequest.Builder
53 |
54 | expect fun Bitmap.asImageBitmap(): ImageBitmap
55 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/components/LudiErrorBox.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.filled.Error
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.graphics.vector.rememberVectorPainter
16 | import androidx.compose.ui.unit.dp
17 | import com.mr3y.ludi.shared.ui.resources.isDesktopPlatform
18 |
19 | @Composable
20 | fun LudiErrorBox(modifier: Modifier = Modifier) {
21 | Box(
22 | contentAlignment = Alignment.Center,
23 | modifier = modifier.background(color = MaterialTheme.colorScheme.surface)
24 | ) {
25 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
26 | Icon(
27 | painter = rememberVectorPainter(image = Icons.Filled.Error),
28 | contentDescription = null,
29 | tint = MaterialTheme.colorScheme.error
30 | )
31 | val errorMessage = if (isDesktopPlatform()) {
32 | "Unexpected Error happened. try to refresh, and see if the problem persists."
33 | } else {
34 | "Unexpected Error happened. pull to refresh, and see if the problem persists."
35 | }
36 | Text(
37 | text = errorMessage,
38 | style = MaterialTheme.typography.titleLarge,
39 | color = MaterialTheme.colorScheme.onSurface
40 | )
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/components/LudiHeaders.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.material3.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.semantics.heading
8 | import androidx.compose.ui.semantics.semantics
9 | import androidx.compose.ui.text.style.TextAlign
10 |
11 | @Composable
12 | fun LudiSectionHeader(
13 | text: String,
14 | modifier: Modifier = Modifier
15 | ) {
16 | Text(
17 | text = text,
18 | color = MaterialTheme.colorScheme.onBackground,
19 | style = MaterialTheme.typography.headlineMedium,
20 | modifier = modifier.semantics {
21 | heading()
22 | },
23 | textAlign = TextAlign.Start
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/components/LudiNavRail.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.ColumnScope
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.WindowInsets
8 | import androidx.compose.foundation.layout.fillMaxHeight
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.widthIn
12 | import androidx.compose.foundation.layout.windowInsetsPadding
13 | import androidx.compose.foundation.selection.selectableGroup
14 | import androidx.compose.material3.NavigationRailDefaults
15 | import androidx.compose.material3.Surface
16 | import androidx.compose.material3.contentColorFor
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.unit.dp
22 |
23 | @Composable
24 | fun LudiNavigationRail(
25 | modifier: Modifier = Modifier,
26 | containerColor: Color = NavigationRailDefaults.ContainerColor,
27 | contentColor: Color = contentColorFor(containerColor),
28 | header: @Composable (ColumnScope.() -> Unit)? = null,
29 | windowInsets: WindowInsets = NavigationRailDefaults.windowInsets,
30 | content: @Composable ColumnScope.() -> Unit
31 | ) {
32 | Surface(
33 | color = containerColor,
34 | contentColor = contentColor,
35 | modifier = modifier
36 | ) {
37 | Column(
38 | Modifier
39 | .fillMaxHeight()
40 | .windowInsetsPadding(windowInsets)
41 | .widthIn(min = 80.dp)
42 | .padding(vertical = 4.dp)
43 | .selectableGroup(),
44 | horizontalAlignment = Alignment.CenterHorizontally,
45 | verticalArrangement = Arrangement.Center
46 | ) {
47 | if (header != null) {
48 | header()
49 | Spacer(Modifier.height(8.dp))
50 | }
51 | content()
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/components/LudiNoInternet.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 |
6 | @Composable
7 | expect fun AnimatedNoInternetBanner(modifier: Modifier = Modifier)
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/components/LudiParallaxAlignment.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components
2 |
3 | import androidx.compose.foundation.lazy.LazyListState
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.Stable
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.unit.IntOffset
9 | import androidx.compose.ui.unit.IntSize
10 | import androidx.compose.ui.unit.LayoutDirection
11 | import kotlin.math.roundToInt
12 |
13 | @Composable
14 | fun rememberParallaxAlignment(
15 | lazyListState: LazyListState,
16 | key: Any?
17 | ): Alignment {
18 | return remember(lazyListState) {
19 | ParallaxAlignment(
20 | horizontalBias = {
21 | if (key == null) {
22 | return@ParallaxAlignment 0f
23 | }
24 |
25 | // Read the LazyListState layout info
26 | val layoutInfo = lazyListState.layoutInfo
27 | // Find the layout info of this item
28 | val itemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.key == key } ?: return@ParallaxAlignment 0f
29 |
30 | val adjustedOffset = itemInfo.offset - layoutInfo.viewportStartOffset
31 | (adjustedOffset / itemInfo.size.toFloat()).coerceIn(-1f, 1f)
32 | }
33 | )
34 | }
35 | }
36 |
37 | @Stable
38 | class ParallaxAlignment(
39 | private val horizontalBias: () -> Float = { 0f },
40 | private val verticalBias: () -> Float = { 0f }
41 | ) : Alignment {
42 | override fun align(
43 | size: IntSize,
44 | space: IntSize,
45 | layoutDirection: LayoutDirection
46 | ): IntOffset {
47 | // Convert to Px first and only round at the end, to avoid rounding twice while calculating
48 | // the new positions
49 | val centerX = (space.width - size.width).toFloat() / 2f
50 | val centerY = (space.height - size.height).toFloat() / 2f
51 | val resolvedHorizontalBias = if (layoutDirection == LayoutDirection.Ltr) {
52 | horizontalBias()
53 | } else {
54 | -1 * horizontalBias()
55 | }
56 |
57 | val x = centerX * (1 + resolvedHorizontalBias)
58 | val y = centerY * (1 + verticalBias())
59 | return IntOffset(x.roundToInt(), y.roundToInt())
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/components/LudiRefreshIconButton.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.foundation.layout.requiredSize
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.filled.Refresh
8 | import androidx.compose.material3.Icon
9 | import androidx.compose.material3.IconButton
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.semantics.contentDescription
15 | import androidx.compose.ui.semantics.semantics
16 | import androidx.compose.ui.unit.dp
17 | import cafe.adriel.lyricist.LocalStrings
18 |
19 | @Composable
20 | fun RefreshIconButton(
21 | onClick: () -> Unit,
22 | modifier: Modifier = Modifier,
23 | tint: Color = MaterialTheme.colorScheme.inverseSurface
24 | ) {
25 | val strings = LocalStrings.current
26 | IconButton(
27 | onClick = onClick,
28 | modifier = modifier
29 | .requiredSize(48.dp)
30 | .semantics {
31 | contentDescription = strings.click_to_refresh
32 | }
33 | ) {
34 | Icon(
35 | imageVector = Icons.Filled.Refresh,
36 | contentDescription = null,
37 | modifier = Modifier
38 | .padding(8.dp)
39 | .fillMaxSize(),
40 | tint = tint
41 | )
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/components/placeholder/LudiPlaceholderModifier.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components.placeholder
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.composed
6 | import androidx.compose.ui.graphics.Color
7 |
8 | fun Modifier.defaultPlaceholder(
9 | isVisible: Boolean,
10 | highlight: PlaceholderHighlight? = null,
11 | color: Color? = null
12 | ): Modifier = composed {
13 | placeholder(
14 | visible = isVisible,
15 | highlight = highlight ?: PlaceholderHighlight.shimmer(),
16 | color = color ?: MaterialTheme.colorScheme.surfaceVariant,
17 | shape = null
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/datastore/FavouriteGamesSerializer.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.datastore
2 |
3 | import androidx.datastore.core.CorruptionException
4 | import androidx.datastore.core.okio.OkioSerializer
5 | import com.mr3y.ludi.datastore.model.UserFavouriteGames
6 | import okio.BufferedSink
7 | import okio.BufferedSource
8 | import okio.IOException
9 |
10 | object FavouriteGamesSerializer : OkioSerializer {
11 | override val defaultValue: UserFavouriteGames = UserFavouriteGames()
12 |
13 | override suspend fun readFrom(source: BufferedSource): UserFavouriteGames {
14 | try {
15 | return UserFavouriteGames.ADAPTER.decode(source)
16 | } catch (exception: IOException) {
17 | throw CorruptionException("Cannot read UserFavouriteGames proto.", exception)
18 | }
19 | }
20 |
21 | override suspend fun writeTo(t: UserFavouriteGames, sink: BufferedSink) {
22 | UserFavouriteGames.ADAPTER.encode(sink, t)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/datastore/FavouriteGenresSerializer.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.datastore
2 |
3 | import androidx.datastore.core.CorruptionException
4 | import androidx.datastore.core.okio.OkioSerializer
5 | import com.mr3y.ludi.datastore.model.UserFavouriteGenres
6 | import okio.BufferedSink
7 | import okio.BufferedSource
8 | import okio.IOException
9 |
10 | object FavouriteGenresSerializer : OkioSerializer {
11 | override val defaultValue: UserFavouriteGenres = UserFavouriteGenres()
12 |
13 | override suspend fun readFrom(source: BufferedSource): UserFavouriteGenres {
14 | try {
15 | return UserFavouriteGenres.ADAPTER.decode(source)
16 | } catch (exception: IOException) {
17 | throw CorruptionException("Cannot read UserFavouriteGenres proto.", exception)
18 | }
19 | }
20 |
21 | override suspend fun writeTo(t: UserFavouriteGenres, sink: BufferedSink) {
22 | UserFavouriteGenres.ADAPTER.encode(sink, t)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/datastore/FollowedNewsDataSourceSerializer.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.datastore
2 |
3 | import androidx.datastore.core.CorruptionException
4 | import androidx.datastore.core.okio.OkioSerializer
5 | import com.mr3y.ludi.datastore.model.FollowedNewsDataSources
6 | import okio.BufferedSink
7 | import okio.BufferedSource
8 | import okio.IOException
9 |
10 | object FollowedNewsDataSourceSerializer : OkioSerializer {
11 | override val defaultValue: FollowedNewsDataSources = FollowedNewsDataSources()
12 |
13 | override suspend fun readFrom(source: BufferedSource): FollowedNewsDataSources {
14 | try {
15 | return FollowedNewsDataSources.ADAPTER.decode(source)
16 | } catch (exception: IOException) {
17 | throw CorruptionException("Cannot read FollowedNewsDataSources proto.", exception)
18 | }
19 | }
20 |
21 | override suspend fun writeTo(t: FollowedNewsDataSources, sink: BufferedSink) {
22 | FollowedNewsDataSources.ADAPTER.encode(sink, t)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/datastore/PreferencesKeys.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.datastore
2 |
3 | import androidx.datastore.preferences.core.booleanPreferencesKey
4 | import androidx.datastore.preferences.core.stringPreferencesKey
5 |
6 | object PreferencesKeys {
7 | val SelectedThemeKey = stringPreferencesKey("selected_theme")
8 | val DynamicColorKey = booleanPreferencesKey("dynamic_color")
9 | val OnBoardingScreenKey = booleanPreferencesKey("show_onboarding_screen")
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/datastore/ProtoDataStoreMutator.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.datastore
2 |
3 | import com.mr3y.ludi.datastore.model.FollowedNewsDataSource
4 | import com.mr3y.ludi.datastore.model.FollowedNewsDataSources
5 | import com.mr3y.ludi.datastore.model.UserFavouriteGame
6 | import com.mr3y.ludi.datastore.model.UserFavouriteGames
7 | import com.mr3y.ludi.datastore.model.UserFavouriteGenre
8 | import com.mr3y.ludi.datastore.model.UserFavouriteGenres
9 | import kotlinx.coroutines.flow.Flow
10 |
11 | interface ProtoDataStoreMutator {
12 |
13 | val followedNewsDataSources: Flow
14 |
15 | val favouriteGames: Flow
16 |
17 | val favouriteGenres: Flow
18 |
19 | suspend fun followNewsDataSource(source: FollowedNewsDataSource)
20 |
21 | suspend fun unFollowNewsDataSource(source: FollowedNewsDataSource)
22 |
23 | suspend fun addGameToFavourites(game: UserFavouriteGame)
24 |
25 | suspend fun removeGameFromFavourites(game: UserFavouriteGame)
26 |
27 | suspend fun addGenreToFavourites(genre: UserFavouriteGenre)
28 |
29 | suspend fun removeGenreFromFavourites(genre: UserFavouriteGenre)
30 | }
31 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/datastore/Wrappers.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.datastore
2 |
3 | import androidx.datastore.core.DataStore
4 | import com.mr3y.ludi.datastore.model.FollowedNewsDataSources
5 | import com.mr3y.ludi.datastore.model.UserFavouriteGames
6 | import com.mr3y.ludi.datastore.model.UserFavouriteGenres
7 |
8 | // kotlin-inject fails to resolve types generated by wire,
9 | // As a workaround we have to wrap those types to make them resolvable.
10 | // See: https://github.com/evant/kotlin-inject/issues/316
11 |
12 | @JvmInline
13 | value class UserFavoriteGamesDataStore(val value: DataStore)
14 |
15 | @JvmInline
16 | value class FollowedNewsDataSourcesDataStore(val value: DataStore)
17 |
18 | @JvmInline
19 | value class UserFavoriteGenresDataStore(val value: DataStore)
20 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/navigation/BottomBarTab.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.navigation
2 |
3 | import androidx.compose.ui.graphics.vector.ImageVector
4 |
5 | interface BottomBarTab {
6 |
7 | val label: String
8 |
9 | val icon: ImageVector
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/navigation/PreferencesType.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.navigation
2 |
3 | enum class PreferencesType {
4 | NewsDataSources,
5 | Genres,
6 | Games
7 | }
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/presenter/NewsFeedThrottler.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.presenter
2 |
3 | import com.mr3y.ludi.shared.di.annotations.Singleton
4 | import me.tatarka.inject.annotations.Inject
5 | import kotlin.time.Duration.Companion.minutes
6 | import kotlin.time.TimeMark
7 | import kotlin.time.TimeSource
8 |
9 | /**
10 | * A simple plain class that has a longer lifetime than our [NewsViewModel] which helps us take correct
11 | * decisions on when to force a data refresh & request updated feed results from network.
12 | * For example, when the NewsViewModel gets popped off the backstack and then the user comes back quickly to NewsScreen,
13 | * we don't want to refresh & request updated data when the previously fetched data is still fresh & valid
14 | */
15 | @Inject
16 | @Singleton
17 | class NewsFeedThrottler(private val timeSource: TimeSource) {
18 |
19 | private var lastUpdateTimeMark: TimeMark? = null
20 |
21 | fun commitSuccessfulUpdate() {
22 | lastUpdateTimeMark = timeSource.markNow()
23 | }
24 |
25 | fun allowRefreshingData() = lastUpdateTimeMark == null || lastUpdateTimeMark!!.elapsedNow() > 30.minutes
26 | }
27 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/presenter/model/DealsState.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.presenter.model
2 |
3 | import app.cash.paging.PagingData
4 | import com.mr3y.ludi.shared.core.model.Deal
5 | import com.mr3y.ludi.shared.core.model.GiveawayEntry
6 | import com.mr3y.ludi.shared.core.model.Result
7 | import com.mr3y.ludi.shared.core.repository.query.DealsSorting
8 | import com.mr3y.ludi.shared.core.repository.query.DealsSortingDirection
9 | import com.mr3y.ludi.shared.core.repository.query.GiveawayPlatform
10 | import com.mr3y.ludi.shared.core.repository.query.GiveawayStore
11 | import com.mr3y.ludi.shared.core.repository.query.GiveawaysSorting
12 | import kotlinx.coroutines.flow.Flow
13 |
14 | data class DealsState(
15 | val deals: Flow>,
16 | val giveaways: Result, Throwable>,
17 | val dealsFiltersState: DealsFiltersState,
18 | val giveawaysFiltersState: GiveawaysFiltersState,
19 | val selectedTab: Int,
20 | val isRefreshingDeals: Boolean,
21 | val isRefreshingGiveaways: Boolean
22 | )
23 |
24 | data class DealsFiltersState(
25 | val currentPage: Int,
26 | val allStores: Set,
27 | val selectedStores: Set,
28 | val sortingCriteria: DealsSorting?,
29 | val sortingDirection: DealsSortingDirection?
30 | )
31 |
32 | data class DealStore(
33 | val id: Int,
34 | val label: String
35 | )
36 |
37 | data class GiveawaysFiltersState(
38 | val allPlatforms: Set,
39 | val selectedPlatforms: Set,
40 | val allStores: Set,
41 | val selectedStores: Set,
42 | val sortingCriteria: GiveawaysSorting?
43 | )
44 |
45 | sealed interface DealsUiEvents {
46 |
47 | data class AddToSelectedDealsStores(val store: DealStore) : DealsUiEvents
48 |
49 | data class RemoveFromSelectedDealsStores(val store: DealStore) : DealsUiEvents
50 |
51 | data class AddToSelectedGiveawaysStores(val store: GiveawayStore) : DealsUiEvents
52 |
53 | data class RemoveFromSelectedGiveawaysStores(val store: GiveawayStore) : DealsUiEvents
54 |
55 | data class AddToSelectedGiveawaysPlatforms(val platform: GiveawayPlatform) : DealsUiEvents
56 |
57 | data class RemoveFromSelectedGiveawaysPlatforms(val platform: GiveawayPlatform) : DealsUiEvents
58 |
59 | data object RefreshDeals : DealsUiEvents
60 |
61 | data object RefreshDealsComplete : DealsUiEvents
62 |
63 | data object RefreshGiveaways : DealsUiEvents
64 |
65 | data class SelectTab(val tabIndex: Int) : DealsUiEvents
66 | }
67 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/presenter/model/DiscoverState.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.presenter.model
2 |
3 | import app.cash.paging.PagingData
4 | import com.mr3y.ludi.shared.core.model.Game
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | data class DiscoverState(
8 | val filtersState: DiscoverFiltersState,
9 | val gamesState: DiscoverStateGames,
10 | val isRefreshing: Boolean
11 | )
12 |
13 | sealed interface DiscoverStateGames {
14 |
15 | data class SuggestedGames(
16 | val trendingGames: Flow>,
17 | val topRatedGames: Flow>,
18 | val favGenresBasedGames: Flow>?,
19 | val multiplayerGames: Flow>,
20 | val freeGames: Flow>,
21 | val storyGames: Flow>,
22 | val boardGames: Flow>,
23 | val eSportsGames: Flow>,
24 | val raceGames: Flow>,
25 | val puzzleGames: Flow>,
26 | val soundtrackGames: Flow>
27 | ) : DiscoverStateGames
28 |
29 | data class SearchQueryBasedGames(
30 | val games: Flow>
31 | ) : DiscoverStateGames
32 | }
33 |
34 | data class DiscoverFiltersState(
35 | val currentPage: Int,
36 | val allPlatforms: Set,
37 | val selectedPlatforms: Set,
38 | val allStores: Set,
39 | val selectedStores: Set,
40 | val allTags: Set,
41 | val selectedTags: Set,
42 | val sortingCriteria: SortingCriteria?
43 | )
44 |
45 | data class Platform(
46 | val id: Int,
47 | val label: String
48 | )
49 |
50 | data class Store(
51 | val id: Int,
52 | val label: String
53 | )
54 |
55 | data class Tag(
56 | val id: Int,
57 | val label: String
58 | )
59 |
60 | data class SortingCriteria(
61 | val criteria: Criteria,
62 | val order: Order
63 | )
64 |
65 | enum class Criteria {
66 | Name,
67 | DateReleased,
68 | DateAdded,
69 | DateCreated,
70 | DateUpdated,
71 | Rating,
72 | Metascore
73 | }
74 | enum class Order {
75 | Ascending,
76 | Descending
77 | }
78 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/presenter/model/EditPreferencesState.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.presenter.model
2 |
3 | import com.mr3y.ludi.shared.core.model.GameGenre
4 | import com.mr3y.ludi.shared.core.model.Result
5 |
6 | sealed interface EditPreferencesState {
7 | @JvmInline
8 | value class FavouriteGames(val favouriteGames: List) : EditPreferencesState
9 | data class FollowedNewsDataSources(val allNewsDataSources: List, val followedNewsDataSources: List) : EditPreferencesState
10 | data class FavouriteGenres(val allGenres: Result, Throwable>, val favouriteGenres: Set) : EditPreferencesState
11 | object Undefined : EditPreferencesState
12 | }
13 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/presenter/model/NewsState.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.presenter.model
2 |
3 | import app.cash.paging.PagingData
4 | import com.mr3y.ludi.shared.core.model.NewReleaseArticle
5 | import com.mr3y.ludi.shared.core.model.NewsArticle
6 | import com.mr3y.ludi.shared.core.model.ReviewArticle
7 | import kotlinx.coroutines.flow.Flow
8 |
9 | data class NewsState(
10 | val isRefreshing: Boolean,
11 | val newsFeed: Flow>,
12 | val reviewsFeed: Flow>,
13 | val newReleasesFeed: Flow>,
14 | val currentEvent: NewsStateEvent? = null
15 | )
16 |
17 | sealed interface NewsStateEvent {
18 | data object FailedToFetchNetworkResults : NewsStateEvent
19 | }
20 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/presenter/model/OnboardingState.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.presenter.model
2 |
3 | import app.cash.paging.PagingData
4 | import com.mr3y.ludi.datastore.model.FollowedNewsDataSource
5 | import com.mr3y.ludi.datastore.model.UserFavouriteGame
6 | import com.mr3y.ludi.datastore.model.UserFavouriteGenre
7 | import com.mr3y.ludi.shared.core.model.Game
8 | import com.mr3y.ludi.shared.core.model.GameGenre
9 | import com.mr3y.ludi.shared.core.model.Result
10 | import com.mr3y.ludi.shared.core.model.Source
11 | import kotlinx.coroutines.flow.Flow
12 | import org.jetbrains.compose.resources.DrawableResource
13 |
14 | data class OnboardingState(
15 | val allNewsDataSources: List,
16 | val followedNewsDataSources: List,
17 | val onboardingGames: OnboardingGames,
18 | val isRefreshingGames: Boolean,
19 | val favouriteGames: List,
20 | val allGamingGenres: Result, Throwable>,
21 | val isRefreshingGenres: Boolean,
22 | val selectedGamingGenres: Set
23 | )
24 |
25 | sealed interface OnboardingGames {
26 | val games: Flow>
27 |
28 | data class SuggestedGames(override val games: Flow>) : OnboardingGames
29 | data class SearchQueryBasedGames(override val games: Flow>) : OnboardingGames
30 | }
31 |
32 | data class NewsDataSource(
33 | val name: String,
34 | val icon: DrawableResource,
35 | val type: Source
36 | )
37 |
38 | internal fun NewsDataSource.toFollowedNewsDataSource(): FollowedNewsDataSource {
39 | return FollowedNewsDataSource(name = name, drawableId = 0, type = type.name)
40 | }
41 |
42 | data class FavouriteGame(
43 | val id: Long,
44 | val title: String,
45 | val imageUrl: String,
46 | val rating: Float
47 | )
48 |
49 | internal fun FavouriteGame.toUserFavouriteGame(): UserFavouriteGame {
50 | return UserFavouriteGame(id = id, name = title, thumbnailUrl = imageUrl, rating = rating)
51 | }
52 |
53 | internal fun GameGenre.toUserFavouriteGenre(): UserFavouriteGenre {
54 | return UserFavouriteGenre(id = id, name = name, imageUrl = imageUrl ?: "", slug = slug ?: "", gamesCount = gamesCount ?: 0L)
55 | }
56 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/presenter/model/SettingsState.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.presenter.model
2 |
3 | data class SettingsState(
4 | val themes: Set,
5 | val selectedTheme: Theme?,
6 | val isUsingDynamicColor: Boolean?
7 | )
8 |
9 | enum class Theme(val label: String) {
10 | Light("Light"),
11 | Dark("Dark"),
12 | SystemDefault("System Default")
13 | }
14 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/presenter/model/SupportedNewsDataSources.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.presenter.model
2 |
3 | import com.mr3y.ludi.shared.core.model.Source
4 | import ludi.shared.generated.resources.Res
5 | import ludi.shared.generated.resources.brutalgamer_logo
6 | import ludi.shared.generated.resources.eurogamer_logo
7 | import ludi.shared.generated.resources.game_spot_logo
8 | import ludi.shared.generated.resources.gamerant_logo
9 | import ludi.shared.generated.resources.giant_bomb_logo
10 | import ludi.shared.generated.resources.gloriousgaming_logo
11 | import ludi.shared.generated.resources.ign_logo
12 | import ludi.shared.generated.resources.pcgamer_logo
13 | import ludi.shared.generated.resources.pcgamesn_logo
14 | import ludi.shared.generated.resources.pcinvasion_logo
15 | import ludi.shared.generated.resources.polygon_logo
16 | import ludi.shared.generated.resources.rockpapershotgun_logo
17 | import ludi.shared.generated.resources.tech_radar_logo
18 | import ludi.shared.generated.resources.thegamer_logo
19 | import ludi.shared.generated.resources.venturebeat_logo
20 | import ludi.shared.generated.resources.vg247_logo
21 |
22 | val SupportedNewsDataSources = listOf(
23 | NewsDataSource("Game spot", Res.drawable.game_spot_logo, Source.GameSpot),
24 | NewsDataSource("Giant bomb", Res.drawable.giant_bomb_logo, Source.GiantBomb),
25 | NewsDataSource("IGN", Res.drawable.ign_logo, Source.IGN),
26 | NewsDataSource("Tech Radar", Res.drawable.tech_radar_logo, Source.TechRadar),
27 | NewsDataSource("PCGamesN", Res.drawable.pcgamesn_logo, Source.PCGamesN),
28 | NewsDataSource("PCGamer", Res.drawable.pcgamer_logo, Source.PCGamer),
29 | NewsDataSource("PC Invasion", Res.drawable.pcinvasion_logo, Source.PCInvasion),
30 | NewsDataSource("Euro Gamer", Res.drawable.eurogamer_logo, Source.EuroGamer),
31 | NewsDataSource("VentureBeat", Res.drawable.venturebeat_logo, Source.VentureBeat),
32 | NewsDataSource("VG247", Res.drawable.vg247_logo, Source.VG247),
33 | NewsDataSource("Glorious Gaming", Res.drawable.gloriousgaming_logo, Source.GloriousGaming),
34 | NewsDataSource("Rock Paper Shotgun", Res.drawable.rockpapershotgun_logo, Source.RockPaperShotgun),
35 | NewsDataSource("Game Rant", Res.drawable.gamerant_logo, Source.GameRant),
36 | NewsDataSource("The Gamer", Res.drawable.thegamer_logo, Source.TheGamer),
37 | NewsDataSource("Polygon", Res.drawable.polygon_logo, Source.Polygon),
38 | NewsDataSource("Brutal Gamer", Res.drawable.brutalgamer_logo, Source.BrutalGamer)
39 | )
40 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/resources/PlatformCompositionProvider.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.resources
2 |
3 | expect fun isDesktopPlatform(): Boolean
4 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/resources/format.kt:
--------------------------------------------------------------------------------
1 | // ktlint-disable filename
2 | package com.mr3y.ludi.shared.ui.resources
3 |
4 | expect fun Float.roundToTwoDecimalDigits(): Float
5 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/theme/ColorScheme.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.theme
2 |
3 | import androidx.compose.material3.ColorScheme
4 | import androidx.compose.runtime.Composable
5 |
6 | @Composable
7 | expect fun colorScheme(
8 | isDarkTheme: Boolean,
9 | useDynamicColors: Boolean
10 | ): ColorScheme
11 |
12 | expect fun isDynamicColorSupported(): Boolean
13 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material3.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(8.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
12 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | labelLarge = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | bodySmall = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
29 |
--------------------------------------------------------------------------------
/shared/src/commonMain/proto/favourite_game.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_package = "com.mr3y.ludi.datastore.model";
4 | option java_multiple_files = true;
5 |
6 | message UserFavouriteGame {
7 | int64 id = 1;
8 | string name = 2;
9 | string thumbnailUrl = 3;
10 | float rating = 4;
11 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/proto/favourite_games.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 | import "favourite_game.proto";
3 |
4 | option java_package = "com.mr3y.ludi.datastore.model";
5 | option java_multiple_files = true;
6 |
7 | message UserFavouriteGames {
8 | repeated UserFavouriteGame favGame = 1;
9 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/proto/favourite_gaming_genre.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_package = "com.mr3y.ludi.datastore.model";
4 | option java_multiple_files = true;
5 |
6 | message UserFavouriteGenre {
7 | int32 id = 1;
8 | string name = 2;
9 | string imageUrl = 3;
10 | int64 gamesCount = 4;
11 | string slug = 5;
12 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/proto/favourite_gaming_genres.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 | import "favourite_gaming_genre.proto";
3 |
4 | option java_package = "com.mr3y.ludi.datastore.model";
5 | option java_multiple_files = true;
6 |
7 | message UserFavouriteGenres {
8 | repeated UserFavouriteGenre favGenre = 1;
9 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/proto/news_data_source.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_package = "com.mr3y.ludi.datastore.model";
4 | option java_multiple_files = true;
5 |
6 | message FollowedNewsDataSource {
7 | string name = 1;
8 | int32 drawableId = 2;
9 | string type = 3;
10 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/proto/news_data_sources.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 | import "news_data_source.proto";
3 |
4 | option java_package = "com.mr3y.ludi.datastore.model";
5 | option java_multiple_files = true;
6 |
7 | message FollowedNewsDataSources {
8 | repeated FollowedNewsDataSource newsDataSource = 1;
9 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/sqldelight/com/mr3y/ludi/shared/Article.sq:
--------------------------------------------------------------------------------
1 | import com.mr3y.ludi.shared.core.model.MarkupText;
2 | import com.mr3y.ludi.shared.core.model.Source;
3 | import com.mr3y.ludi.shared.core.model.Title;
4 | import java.time.ZonedDateTime;
5 |
6 | CREATE TABLE IF NOT EXISTS articleEntity (
7 | title TEXT AS Title PRIMARY KEY NOT NULL,
8 | description TEXT AS MarkupText,
9 | source TEXT AS Source NOT NULL,
10 | sourceLinkUrl TEXT NOT NULL,
11 | content TEXT AS MarkupText,
12 | imageUrl TEXT,
13 | author TEXT,
14 | publicationDate TEXT AS ZonedDateTime,
15 | type TEXT NOT NULL
16 | );
17 |
18 | CREATE INDEX IF NOT EXISTS idx_type ON articleEntity (type);
19 |
20 | queryArticles:
21 | SELECT * FROM articleEntity WHERE type == :type ORDER BY
22 | CASE WHEN type == "new_releases" THEN publicationDate END ASC,
23 | CASE WHEN type == "news" OR type == "reviews" THEN publicationDate END DESC,
24 | title ASC LIMIT :limit OFFSET :offset;
25 |
26 | countArticles:
27 | SELECT count(*) FROM articleEntity WHERE type == :type;
28 |
29 | insertArticle:
30 | INSERT OR REPLACE INTO articleEntity(title, description, source, sourceLinkUrl, content, imageUrl, author, publicationDate, type) VALUES ?;
31 |
32 | deleteArticlesByType:
33 | DELETE FROM articleEntity WHERE type = :type;
34 |
--------------------------------------------------------------------------------
/shared/src/commonMain/sqldelight/com/mr3y/ludi/shared/ArticleSearchFTS.sq:
--------------------------------------------------------------------------------
1 | CREATE VIRTUAL TABLE IF NOT EXISTS articleEntitySearch USING FTS5(
2 | title TEXT NOT NULL,
3 | description TEXT,
4 | source TEXT NOT NULL,
5 | sourceLinkUrl TEXT NOT NULL UNINDEXED,
6 | content TEXT,
7 | author TEXT,
8 | type TEXT NOT NULL UNINDEXED,
9 | tokenize="trigram"
10 | );
11 |
12 | CREATE TRIGGER IF NOT EXISTS
13 | articleEntitySearch_BEFORE_DELETE
14 | BEFORE DELETE ON articleEntity BEGIN DELETE FROM articleEntitySearch
15 | WHERE sourceLinkUrl = old.sourceLinkUrl;
16 | END;
17 |
18 | CREATE TRIGGER IF NOT EXISTS
19 | articleEntitySearch_AFTER_INSERT
20 | AFTER INSERT ON articleEntity
21 | BEGIN INSERT OR REPLACE INTO articleEntitySearch(title, description, source, sourceLinkUrl, content, author, type) VALUES (new.title, new.description, new.source, new.sourceLinkUrl, new.content, new.author, new.type);
22 | END;
23 |
24 | countSearchResults:
25 | SELECT count(*) FROM articleEntitySearch WHERE articleEntitySearch MATCH :searchQuery AND type == :type;
26 |
27 | search:
28 | SELECT
29 | articleEntitySearch.title,
30 | articleEntitySearch.description,
31 | articleEntitySearch.source,
32 | articleEntitySearch.content,
33 | articleEntitySearch.author,
34 | articleEntity.sourceLinkUrl,
35 | articleEntity.imageUrl,
36 | articleEntity.publicationDate,
37 | articleEntity.type
38 | FROM articleEntitySearch
39 | INNER JOIN articleEntity ON articleEntity.sourceLinkUrl == articleEntitySearch.sourceLinkUrl
40 | WHERE articleEntitySearch MATCH :searchQuery AND articleEntity.type == :type
41 | ORDER BY bm25(articleEntitySearch) DESC
42 | LIMIT :limit OFFSET :offset;
--------------------------------------------------------------------------------
/shared/src/commonMain/sqldelight/databases/1.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/shared/src/commonMain/sqldelight/databases/1.db
--------------------------------------------------------------------------------
/shared/src/commonMain/sqldelight/databases/2.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mr3y-the-programmer/Ludi/2b10921a04f3639ccd31d491849f8e05b36e65aa/shared/src/commonMain/sqldelight/databases/2.db
--------------------------------------------------------------------------------
/shared/src/commonMain/sqldelight/migrations/1.sqm:
--------------------------------------------------------------------------------
1 | CREATE VIRTUAL TABLE IF NOT EXISTS articleEntitySearch USING FTS5(
2 | title TEXT NOT NULL,
3 | description TEXT,
4 | source TEXT NOT NULL,
5 | sourceLinkUrl TEXT NOT NULL UNINDEXED,
6 | content TEXT,
7 | author TEXT,
8 | type TEXT NOT NULL UNINDEXED,
9 | tokenize="trigram"
10 | );
11 |
12 | INSERT INTO articleEntitySearch SELECT title, description, source, sourceLinkUrl, content, author, type FROM articleEntity;
13 |
14 | CREATE TRIGGER IF NOT EXISTS
15 | articleEntitySearch_BEFORE_DELETE
16 | BEFORE DELETE ON articleEntity BEGIN DELETE FROM articleEntitySearch
17 | WHERE sourceLinkUrl = old.sourceLinkUrl;
18 | END;
19 |
20 | CREATE TRIGGER IF NOT EXISTS
21 | articleEntitySearch_AFTER_INSERT
22 | AFTER INSERT ON articleEntity
23 | BEGIN INSERT OR REPLACE INTO articleEntitySearch(title, description, source, sourceLinkUrl, content, author, type) VALUES (new.title, new.description, new.source, new.sourceLinkUrl, new.content, new.author, new.type);
24 | END;
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/com/mr3y/ludi/shared/MainDispatcherRule.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.ExperimentalCoroutinesApi
5 | import kotlinx.coroutines.test.TestDispatcher
6 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
7 | import kotlinx.coroutines.test.resetMain
8 | import kotlinx.coroutines.test.setMain
9 | import org.junit.rules.TestWatcher
10 | import org.junit.runner.Description
11 |
12 | /**
13 | * A JUnit [TestRule] that sets the Main dispatcher to [testDispatcher]
14 | * for the duration of the test.
15 | */
16 | @OptIn(ExperimentalCoroutinesApi::class)
17 | class MainDispatcherRule(
18 | val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
19 | ) : TestWatcher() {
20 | override fun starting(description: Description) {
21 | Dispatchers.setMain(testDispatcher)
22 | }
23 |
24 | override fun finished(description: Description) {
25 | Dispatchers.resetMain()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/com/mr3y/ludi/shared/core/FakeCrashReporting.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core
2 |
3 | class FakeCrashReporting : CrashReporting {
4 |
5 | private val exceptions = mutableListOf()
6 |
7 | override fun recordException(throwable: Throwable, logMessage: String?) {
8 | println(logMessage)
9 | exceptions += throwable
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/com/mr3y/ludi/shared/core/network/fixtures/TestLogger.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.network.fixtures
2 |
3 | import com.mr3y.ludi.shared.core.Logger
4 |
5 | class TestLogger : Logger {
6 |
7 | private val _logs = mutableListOf()
8 | val logs: List
9 | get() = _logs
10 |
11 | override fun d(throwable: Throwable?, tag: String, message: () -> String) {
12 | val evaluatedMessage = message()
13 | _logs += evaluatedMessage
14 | println("$tag: $evaluatedMessage")
15 | }
16 |
17 | override fun i(throwable: Throwable?, tag: String, message: () -> String) {
18 | val evaluatedMessage = message()
19 | _logs += evaluatedMessage
20 | println("$tag: $evaluatedMessage")
21 | }
22 |
23 | override fun e(throwable: Throwable?, tag: String, message: () -> String) {
24 | val evaluatedMessage = message()
25 | _logs += evaluatedMessage
26 | println("$tag: $evaluatedMessage - $throwable")
27 | }
28 |
29 | override fun v(throwable: Throwable?, tag: String, message: () -> String) {
30 | val evaluatedMessage = message()
31 | _logs += evaluatedMessage
32 | println("$tag: $evaluatedMessage")
33 | }
34 |
35 | override fun w(throwable: Throwable?, tag: String, message: () -> String) {
36 | val evaluatedMessage = message()
37 | _logs += evaluatedMessage
38 | println("$tag: $evaluatedMessage - $throwable")
39 | }
40 |
41 | fun reset() {
42 | _logs.clear()
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/com/mr3y/ludi/shared/core/network/rssparser/FakeRSSParser.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core.network.rssparser
2 |
3 | import com.mr3y.ludi.shared.core.model.Article
4 | import com.mr3y.ludi.shared.core.model.NewReleaseArticle
5 | import com.mr3y.ludi.shared.core.model.NewsArticle
6 | import com.mr3y.ludi.shared.core.model.ReviewArticle
7 | import com.mr3y.ludi.shared.core.model.Source
8 |
9 | class FakeRSSParser : Parser {
10 |
11 | private val cache = hashMapOf>()
12 | private var fail = false
13 |
14 | fun setNewsArticlesAtUrl(url: String, articles: () -> List) {
15 | cache[url] = articles()
16 | }
17 |
18 | fun setReviewArticlesAtUrl(url: String, articles: () -> List) {
19 | cache[url] = articles()
20 | }
21 |
22 | fun setNewReleaseArticlesAtUrl(url: String, articles: () -> List) {
23 | cache[url] = articles()
24 | }
25 |
26 | override suspend fun parseNewsArticlesAtUrl(url: String, source: Source): List {
27 | if (fail) {
28 | throw Exception("Simulated Failure")
29 | }
30 | return cache[url] as? List ?: emptyList()
31 | }
32 |
33 | override suspend fun parseReviewArticlesAtUrl(url: String, source: Source): List {
34 | if (fail) {
35 | throw Exception("Simulated Failure")
36 | }
37 | return cache[url] as? List ?: emptyList()
38 | }
39 |
40 | override suspend fun parseNewReleaseArticlesAtUrl(url: String, source: Source): List {
41 | if (fail) {
42 | throw Exception("Simulated Failure")
43 | }
44 | return cache[url] as? List ?: emptyList()
45 | }
46 |
47 | fun simulateFailure() {
48 | fail = true
49 | }
50 |
51 | fun reset() {
52 | fail = false
53 | cache.clear()
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/com/mr3y/ludi/shared/ui/presenter/FakeDiscoverPagingFactory.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.presenter
2 |
3 | import app.cash.paging.PagingData
4 | import com.mr3y.ludi.shared.core.model.Game
5 | import com.mr3y.ludi.shared.core.repository.fixtures.FakeGamesRepository
6 | import com.mr3y.ludi.shared.core.repository.query.GamesQuery
7 | import com.mr3y.ludi.shared.ui.presenter.model.DiscoverFiltersState
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | class FakeDiscoverPagingFactory(private val repo: FakeGamesRepository) : DiscoverPagingFactory {
11 | override val trendingGamesPager: Flow> = repo.queryGames(GamesQuery())
12 |
13 | override val topRatedGamesPager: Flow> = repo.queryGames(GamesQuery())
14 |
15 | override val favGenresBasedGamesPager: Flow> = repo.queryGames(GamesQuery())
16 |
17 | override val multiplayerGamesPager: Flow> = repo.queryGames(GamesQuery())
18 |
19 | override val freeGamesPager: Flow> = repo.queryGames(GamesQuery())
20 |
21 | override val storyGamesPager: Flow> = repo.queryGames(GamesQuery())
22 |
23 | override val boardGamesPager: Flow> = repo.queryGames(GamesQuery())
24 |
25 | override val esportsGamesPager: Flow> = repo.queryGames(GamesQuery())
26 |
27 | override val raceGamesPager: Flow> = repo.queryGames(GamesQuery())
28 |
29 | override val puzzleGamesPager: Flow> = repo.queryGames(GamesQuery())
30 |
31 | override val soundtrackGamesPager: Flow> = repo.queryGames(GamesQuery())
32 |
33 | override fun searchQueryBasedGamesPager(
34 | searchQuery: String,
35 | filters: DiscoverFiltersState
36 | ): Flow> {
37 | return repo.queryGames(GamesQuery(searchQuery = searchQuery))
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/shared/src/desktopAndroidMain/kotlin/com/mr3y/ludi/shared/ui/resources/format.shared.kt:
--------------------------------------------------------------------------------
1 | // ktlint-disable filename
2 | package com.mr3y.ludi.shared.ui.resources
3 |
4 | import java.math.RoundingMode
5 |
6 | actual fun Float.roundToTwoDecimalDigits(): Float = toBigDecimal().setScale(2, RoundingMode.HALF_UP).toFloat()
7 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/AppCacheDir.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared
2 |
3 | import java.io.File
4 |
5 | fun getCacheDir() = when (currentOperatingSystem) {
6 | OperatingSystem.Windows -> File(System.getenv("AppData"), "Ludi/cache")
7 | OperatingSystem.Linux -> File(System.getProperty("user.home"), ".cache/Ludi")
8 | OperatingSystem.MacOS -> File(System.getProperty("user.home"), "Library/Caches/Ludi")
9 | else -> throw IllegalStateException("Unsupported operating system")
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/AppDir.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared
2 |
3 | import java.io.File
4 |
5 | enum class OperatingSystem {
6 | Windows, Linux, MacOS, Unknown
7 | }
8 |
9 | val currentOperatingSystem: OperatingSystem
10 | get() {
11 | val operSys = System.getProperty("os.name").lowercase()
12 | return if (operSys.contains("win")) {
13 | OperatingSystem.Windows
14 | } else if (operSys.contains("nix") || operSys.contains("nux") ||
15 | operSys.contains("aix")
16 | ) {
17 | OperatingSystem.Linux
18 | } else if (operSys.contains("mac")) {
19 | OperatingSystem.MacOS
20 | } else {
21 | OperatingSystem.Unknown
22 | }
23 | }
24 |
25 | fun getAppDir() = when (currentOperatingSystem) {
26 | OperatingSystem.Windows -> File(System.getenv("AppData"), "LudiApp/Ludi")
27 | OperatingSystem.Linux -> File(System.getProperty("user.home"), ".local/share/Ludi")
28 | OperatingSystem.MacOS -> File(System.getProperty("user.home"), "Library/Application Support/Ludi")
29 | else -> throw IllegalStateException("Unsupported operating system")
30 | }
31 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/core/BugsnagReporting.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.core
2 |
3 | import com.bugsnag.Bugsnag
4 | import me.tatarka.inject.annotations.Inject
5 |
6 | @Inject
7 | class BugsnagReporting(
8 | private val bugsnagClient: Bugsnag
9 | ) : CrashReporting {
10 |
11 | override fun recordException(throwable: Throwable, logMessage: String?) {
12 | bugsnagClient.notify(throwable)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/core/database/DatabaseFactory.desktop.kt:
--------------------------------------------------------------------------------
1 | // ktlint-disable filename
2 | package com.mr3y.ludi.shared.core.database
3 |
4 | import app.cash.sqldelight.async.coroutines.synchronous
5 | import app.cash.sqldelight.db.SqlDriver
6 | import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
7 | import com.mr3y.ludi.shared.getAppDir
8 | import me.tatarka.inject.annotations.Inject
9 | import java.io.File
10 |
11 | @Inject
12 | actual class DriverFactory {
13 | actual fun createDriver(): SqlDriver {
14 | val databaseFile = File(getAppDir(), "ludi_database.db")
15 | val driver: SqlDriver = JdbcSqliteDriver(url = "jdbc:sqlite:${databaseFile.path}")
16 | LudiDatabase.Schema.synchronous().create(driver)
17 | return driver
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/DatabaseDispatcherComponent.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.Singleton
4 | import kotlinx.coroutines.Dispatchers
5 | import me.tatarka.inject.annotations.Provides
6 |
7 | actual interface DatabaseDispatcherComponent {
8 |
9 | @Singleton
10 | @Provides
11 | fun provideMainDispatcher(): DatabaseDispatcher {
12 | return DatabaseDispatcher(Dispatchers.Main)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/DealsFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.DealsFeatureScope
4 | import me.tatarka.inject.annotations.Component
5 |
6 | @Component
7 | @DealsFeatureScope
8 | abstract class DealsFeatureComponent(
9 | @Component val parent: HostWindowComponent
10 | ) : SharedDealsFeatureComponent
11 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/DesktopApplicationComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.Singleton
4 | import com.mr3y.ludi.shared.getAppDir
5 | import com.mr3y.ludi.shared.getCacheDir
6 | import me.tatarka.inject.annotations.Component
7 | import okio.Path
8 | import okio.Path.Companion.toOkioPath
9 | import java.io.File
10 |
11 | @Component
12 | @Singleton
13 | abstract class DesktopApplicationComponent : SharedApplicationComponent, DesktopCrashReportingComponent, DesktopSqlDriverComponent {
14 |
15 | override val dataStoreParentDir: Path = getAppDir().toOkioPath()
16 |
17 | override val okhttpCacheParentDir: File = getCacheDir()
18 |
19 | companion object
20 | }
21 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/DesktopCrashReportingComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.bugsnag.Bugsnag
4 | import com.mr3y.ludi.shared.DesktopMainBuildConfig
5 | import com.mr3y.ludi.shared.core.BugsnagReporting
6 | import com.mr3y.ludi.shared.core.CrashReporting
7 | import com.mr3y.ludi.shared.di.annotations.Singleton
8 | import me.tatarka.inject.annotations.Provides
9 |
10 | interface DesktopCrashReportingComponent {
11 |
12 | @Singleton
13 | @Provides
14 | fun provideBugsnagClientInstance(): Bugsnag {
15 | return Bugsnag(DesktopMainBuildConfig.BUGSNAG_API_KEY)
16 | }
17 |
18 | @Singleton
19 | @Provides
20 | fun provideCrashReportingInstance(bugsnagClient: Bugsnag): CrashReporting {
21 | return BugsnagReporting(bugsnagClient)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/DesktopSqlDriverComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.core.database.DriverFactory
4 | import com.mr3y.ludi.shared.di.annotations.Singleton
5 | import me.tatarka.inject.annotations.Provides
6 |
7 | interface DesktopSqlDriverComponent {
8 |
9 | @Singleton
10 | @Provides
11 | fun provideJdbcSqlDriverFactoryInstance(): DriverFactory {
12 | return DriverFactory()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/DiscoverFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.DiscoverFeatureScope
4 | import me.tatarka.inject.annotations.Component
5 |
6 | @Component
7 | @DiscoverFeatureScope
8 | abstract class DiscoverFeatureComponent(
9 | @Component val parent: HostWindowComponent
10 | ) : SharedDiscoverFeatureComponent
11 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/HostWindowComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import androidx.compose.runtime.staticCompositionLocalOf
4 | import androidx.compose.ui.awt.ComposeWindow
5 | import com.mr3y.ludi.shared.ui.navigation.PreferencesType
6 | import com.mr3y.ludi.shared.ui.presenter.EditPreferencesViewModel
7 | import me.tatarka.inject.annotations.Component
8 | import me.tatarka.inject.annotations.Provides
9 |
10 | @Component
11 | abstract class HostWindowComponent(
12 | @get:Provides val window: ComposeWindow,
13 | @Component val parent: DesktopApplicationComponent
14 | ) {
15 | abstract val editPreferencesViewModelFactory: (PreferencesType) -> EditPreferencesViewModel
16 | }
17 |
18 | val LocalHostWindowComponent = staticCompositionLocalOf { error("HostWindowComponent isn't provided!") }
19 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/NewsFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.NewsFeatureScope
4 | import me.tatarka.inject.annotations.Component
5 |
6 | @Component
7 | @NewsFeatureScope
8 | abstract class NewsFeatureComponent(
9 | @Component val parent: HostWindowComponent
10 | ) : SharedNewsFeatureComponent
11 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/OnboardingFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.OnboardingFeatureScope
4 | import me.tatarka.inject.annotations.Component
5 |
6 | @Component
7 | @OnboardingFeatureScope
8 | abstract class OnboardingFeatureComponent(
9 | @Component val parent: HostWindowComponent
10 | ) : SharedOnboardingFeatureComponent
11 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/SettingsFeatureComponent.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.SettingsFeatureScope
4 | import me.tatarka.inject.annotations.Component
5 |
6 | @Component
7 | @SettingsFeatureScope
8 | abstract class SettingsFeatureComponent(
9 | @Component val parent: HostWindowComponent
10 | ) : SharedSettingsFeatureComponent
11 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/di/TimeSourceComponent.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.di
2 |
3 | import com.mr3y.ludi.shared.di.annotations.Singleton
4 | import me.tatarka.inject.annotations.Provides
5 | import kotlin.time.TimeSource
6 |
7 | actual interface TimeSourceComponent {
8 | @Provides
9 | @Singleton
10 | fun provideTimeSourceInstance(): TimeSource = TimeSource.Monotonic
11 | }
12 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/components/LudiAsyncImage.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components
2 |
3 | import androidx.compose.ui.graphics.ImageBitmap
4 | import androidx.compose.ui.graphics.asComposeImageBitmap
5 | import coil3.Bitmap
6 | import coil3.request.ImageRequest
7 |
8 | actual fun ImageRequest.Builder.platformSpecificConfig(allowHardware: Boolean): ImageRequest.Builder = this
9 |
10 | actual fun Bitmap.asImageBitmap(): ImageBitmap = this.asComposeImageBitmap()
11 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/components/LudiNoInternet.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 |
6 | @Composable
7 | actual fun AnimatedNoInternetBanner(modifier: Modifier) { }
8 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/components/OpenUrls.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.components
2 |
3 | import java.awt.Desktop
4 | import java.net.URI
5 |
6 | fun openUrlInBrowser(url: String) {
7 | Desktop.getDesktop().browse(URI.create(url))
8 | }
9 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/presenter/FrameClock.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.presenter
2 |
3 | import androidx.compose.runtime.BroadcastFrameClock
4 | import kotlin.coroutines.CoroutineContext
5 |
6 | internal actual fun frameClock(): CoroutineContext = BroadcastFrameClock()
7 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/resources/PlatformCompositionProvider.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.resources
2 |
3 | actual fun isDesktopPlatform() = true
4 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/screens/deals/DealsScreen.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.screens.deals
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.ui.Modifier
7 | import com.mr3y.ludi.shared.ui.components.openUrlInBrowser
8 | import com.mr3y.ludi.shared.ui.presenter.DealsViewModel
9 |
10 | @Composable
11 | actual fun DealsScreen(
12 | modifier: Modifier,
13 | viewModel: DealsViewModel
14 | ) {
15 | val dealsState by viewModel.dealsState.collectAsState()
16 | DealsScreen(
17 | dealsState = dealsState,
18 | searchQuery = viewModel.searchQuery.value,
19 | modifier = modifier,
20 | onUpdateSearchQuery = viewModel::updateSearchQuery,
21 | onSelectingDealStore = viewModel::addToSelectedDealsStores,
22 | onUnselectingDealStore = viewModel::removeFromSelectedDealsStores,
23 | onSelectingGiveawayStore = viewModel::addToSelectedGiveawaysStores,
24 | onUnselectingGiveawayStore = viewModel::removeFromSelectedGiveawaysStores,
25 | onSelectingGiveawayPlatform = viewModel::addToSelectedGiveawaysPlatforms,
26 | onUnselectingGiveawayPlatform = viewModel::removeFromSelectedGiveawayPlatforms,
27 | onRefreshDeals = viewModel::refreshDeals,
28 | onRefreshDealsFinished = viewModel::refreshDealsComplete,
29 | onRefreshGiveaways = viewModel::refreshGiveaways,
30 | onSelectTab = viewModel::selectTab,
31 | onOpenUrl = ::openUrlInBrowser
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/screens/discover/DiscoverScreen.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.screens.discover
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.ui.Modifier
7 | import com.mr3y.ludi.shared.ui.components.openUrlInBrowser
8 | import com.mr3y.ludi.shared.ui.presenter.DiscoverViewModel
9 |
10 | @Composable
11 | actual fun DiscoverScreen(
12 | modifier: Modifier,
13 | viewModel: DiscoverViewModel
14 | ) {
15 | val discoverState by viewModel.discoverState.collectAsState()
16 | DiscoverScreen(
17 | discoverState = discoverState,
18 | searchQuery = viewModel.searchQuery.value,
19 | onUpdatingSearchQueryText = viewModel::updateSearchQuery,
20 | onSelectingPlatform = viewModel::addToSelectedPlatforms,
21 | onUnselectingPlatform = viewModel::removeFromSelectedPlatforms,
22 | onSelectingStore = viewModel::addToSelectedStores,
23 | onUnselectingStore = viewModel::removeFromSelectedStores,
24 | onSelectingTag = viewModel::addToSelectedTags,
25 | onUnselectingTag = viewModel::removeFromSelectedTags,
26 | onRefresh = viewModel::refresh,
27 | onRefreshFinished = viewModel::refreshComplete,
28 | onOpenUrl = ::openUrlInBrowser,
29 | modifier = modifier
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/screens/news/NewsScreen.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.screens.news
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.ui.Modifier
7 | import com.mr3y.ludi.shared.ui.components.openUrlInBrowser
8 | import com.mr3y.ludi.shared.ui.presenter.NewsViewModel
9 |
10 | @Composable
11 | actual fun NewsScreen(
12 | onTuneClick: () -> Unit,
13 | modifier: Modifier,
14 | viewModel: NewsViewModel
15 | ) {
16 | val newsState by viewModel.newsState.collectAsState()
17 | NewsScreen(
18 | newsState = newsState,
19 | searchQuery = viewModel.searchQuery.value,
20 | onSearchQueryValueChanged = viewModel::updateSearchQuery,
21 | onTuneClick = onTuneClick,
22 | onRefresh = viewModel::refresh,
23 | onConsumeEvent = viewModel::consumeCurrentEvent,
24 | onOpenUrl = ::openUrlInBrowser,
25 | modifier = modifier
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/screens/settings/SettingsScreen.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.screens.settings
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.collectAsState
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.ui.Modifier
7 | import com.mr3y.ludi.shared.ui.components.openUrlInBrowser
8 | import com.mr3y.ludi.shared.ui.presenter.SettingsViewModel
9 |
10 | @Composable
11 | actual fun SettingsScreen(
12 | onFollowedNewsDataSourcesClick: () -> Unit,
13 | onFavouriteGamesClick: () -> Unit,
14 | onFavouriteGenresClick: () -> Unit,
15 | modifier: Modifier,
16 | viewModel: SettingsViewModel
17 | ) {
18 | val settingsState by viewModel.settingsState.collectAsState()
19 | SettingsScreen(
20 | settingsState,
21 | modifier = modifier,
22 | onFollowedNewsDataSourcesClick = onFollowedNewsDataSourcesClick,
23 | onFavouriteGamesClick = onFavouriteGamesClick,
24 | onFavouriteGenresClick = onFavouriteGenresClick,
25 | onUpdateTheme = viewModel::setAppTheme,
26 | onToggleDynamicColorValue = {},
27 | onOpenUrl = ::openUrlInBrowser
28 | )
29 | }
30 |
31 | actual fun isDynamicColorEnabled(): Boolean = false
32 |
--------------------------------------------------------------------------------
/shared/src/desktopMain/kotlin/com/mr3y/ludi/shared/ui/theme/ColorScheme.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.mr3y.ludi.shared.ui.theme
2 |
3 | import androidx.compose.material3.ColorScheme
4 | import androidx.compose.runtime.Composable
5 |
6 | @Composable
7 | actual fun colorScheme(
8 | isDarkTheme: Boolean,
9 | useDynamicColors: Boolean
10 | ): ColorScheme {
11 | return if (isDarkTheme) {
12 | DarkColorScheme
13 | } else {
14 | LightColorScheme
15 | }
16 | }
17 |
18 | actual fun isDynamicColorSupported() = false
19 |
--------------------------------------------------------------------------------
/versions.properties:
--------------------------------------------------------------------------------
1 | #### Dependencies and Plugin versions with their available updates.
2 | #### Generated by `./gradlew refreshVersions` version 0.60.5
3 | ####
4 | #### Don't manually edit or split the comments that start with four hashtags (####),
5 | #### they will be overwritten by refreshVersions.
6 | ####
7 | #### suppress inspection "SpellCheckingInspection" for whole file
8 | #### suppress inspection "UnusedProperty" for whole file
9 | ####
10 | #### NOTE: Some versions are filtered by the rejectVersionIf predicate. See the settings.gradle.kts file.
11 |
--------------------------------------------------------------------------------