From 45a534b360ceb425a70890dcd4a0d7944e5066ec Mon Sep 17 00:00:00 2001
From: Sarath S <53013721+Sharkaboi@users.noreply.github.com>
Date: Sat, 14 Aug 2021 00:01:06 +0530
Subject: [PATCH] V1.3 (#32)
- Updated CI to run instrumentation tests and have better caching mechanism
- Upgraded project to use JDK 11
- Moved all dependency versions to ext variable set
- Bumped target and compile SDK to API 31
- Refactored `PresentationExtensions` and Enum classes to use string resources. Closes #13
- Added instrumentation tests for `PresentationExtensions`
- Fixed small formatting issues with text
- Removed useless GraphQl calls for next episode airing time
- Handled invalid deeplinks using regex by querying default browser of phone and force opening url. Closes #30
- Added tests for accepted deeplink regex
- Refactored `openAnimeById`, `openMangaById` and `openImageSlider` to global navigation actions
- Removed dead code
- Refactored standalone companion object classes to objects
- Extracted `UIConstants` to reuse chip styles, Image transformations, Corner radius and grid count
- Added `printStackTrace()` calls to missed catch blocks
- Cleaned up `MPAndroidChart` value formatter to use ktx functions
- Refactored `Uri.parse(url)` calls to `url.toUri()`
- Moved api constants and `MALExternalLinks` to data package
- Added `AnimeAiringStatus`, `AnimeNsfwRating`, `AnimeRating`, `MangaNsfwRating`, `MangaPublishingStatus` enums and tests
- Forced all paging data sources to work on `IO` thread
- Forced Datastore to use `IO` thread
- Refactored `MHError` to have general static error types
- Removed magic numbers to enum classes
- Fixed bug with 0 `timeToAiring` of episode returning invalid string
- Decomposed UI binding functions to smaller functions for readability
- Fixed Anime Seasonal screen not loading on navigation with season args
- Fixed Anime Ranking screen not loading on navigation with ranking args
- Fixed Manga Ranking screen not loading on navigation with ranking args
- Fixed incompatible manga ranking type slugs across manga service and manga rank service by mapping
- Refactored `OAuthRepository` to use `MHTaskState`
- Move fab explode animation to after navigation
- Rearrange `strings.xml `
- Cleaned up comments
- Updated `Readme` to use latest kotlin version and changed project info section
---
.github/workflows/android.yaml | 99 ++-
README.md | 36 +-
app/build.gradle | 12 +-
app/src/androidTest/java/EnumTest.kt | 195 ++++++
.../java/PresentationExtensionsTest.kt | 559 ++++++++++++++++
app/src/main/AndroidManifest.xml | 6 +
.../graphql/nextAiringAnimeEpisode.graphql | 4 -
.../alarm_manager/NotifyAlarmManager.kt | 12 -
.../mediahub/common/constants/AppConstants.kt | 15 +-
.../common/constants/MALExternalLinks.kt | 105 ---
.../mediahub/common/constants/UIConstants.kt | 52 ++
.../extensions/KotlinResultExtension.kt | 5 +-
.../extensions/PresentationExtensions.kt | 478 +++++--------
.../common/extensions/StringExtensions.kt | 14 +-
.../common/extensions/UtilExtensions.kt | 4 +
.../util/MPAndroidChartValueFormatter.kt | 35 +-
.../mediahub/common/util/UrlLauncher.kt | 55 +-
.../common/views/CustomProgressScreen.kt | 2 +-
.../image_slider/FullScreenImageFragment.kt | 3 +-
.../data/api/{ => constants}/ApiConstants.kt | 3 +-
.../data/api/constants/MALExternalLinks.kt | 115 ++++
.../data/api/enums/AnimeAiringStatus.kt | 29 +
.../data/api/enums/AnimeNsfwRating.kt | 30 +
.../data/api/enums/AnimeRankingType.kt | 23 +-
.../mediahub/data/api/enums/AnimeRating.kt | 26 +
.../mediahub/data/api/enums/AnimeStatus.kt | 27 +-
.../data/api/enums/MangaNsfwRating.kt | 20 +
.../data/api/enums/MangaPublishingStatus.kt | 19 +
.../data/api/enums/MangaRankingType.kt | 40 +-
.../mediahub/data/api/enums/MangaStatus.kt | 27 +-
.../data/api/enums/UserAnimeSortType.kt | 13 +-
.../data/api/enums/UserMangaSortType.kt | 13 +-
.../data/api/retrofit/AnimeService.kt | 2 +-
.../data/api/retrofit/MangaService.kt | 2 +-
.../data/api/retrofit/UserAnimeService.kt | 2 +-
.../data/api/retrofit/UserMangaService.kt | 2 +-
.../mediahub/data/api/retrofit/UserService.kt | 2 +-
.../data/datastore/DataStoreRepositoryImpl.kt | 12 +-
.../data/paging/AnimeRankingDataSource.kt | 36 +-
.../data/paging/AnimeSearchDataSource.kt | 36 +-
.../data/paging/AnimeSeasonalDataSource.kt | 38 +-
.../data/paging/AnimeSuggestionsDataSource.kt | 34 +-
.../data/paging/MangaRankingDataSource.kt | 36 +-
.../data/paging/MangaSearchDataSource.kt | 36 +-
.../data/paging/UserAnimeListDataSource.kt | 38 +-
.../data/paging/UserMangaListDataSource.kt | 38 +-
.../data/sharedpref/SharedPreferencesKeys.kt | 18 +-
.../mediahub/data/wrappers/MHError.kt | 17 +-
.../data/wrappers/NoTokenFoundError.kt | 9 -
.../mediahub/di/AlarmManagerModule.kt | 20 -
.../anime/adapters/AnimeListAdapter.kt | 26 +-
.../anime/adapters/AnimePagerAdapter.kt | 2 +-
.../anime/repository/AnimeRepositoryImpl.kt | 8 +-
.../modules/anime/ui/AnimeFragment.kt | 3 +-
.../anime/ui/AnimeListByStatusFragment.kt | 13 +-
.../adapters/RecommendedAnimeAdapter.kt | 19 +-
.../adapters/RelatedAnimeAdapter.kt | 14 +-
.../adapters/RelatedMangaAdapter.kt | 14 +-
.../repository/AnimeDetailsRepositoryImpl.kt | 78 +--
.../anime_details/ui/AnimeDetailsFragment.kt | 580 ++++++++--------
.../anime_details/vm/AnimeDetailsViewModel.kt | 3 +-
.../adapters/AnimeRankingDetailedAdapter.kt | 22 +-
.../repository/AnimeRankingRepositoryImpl.kt | 2 +-
.../anime_ranking/ui/AnimeRankingFragment.kt | 11 +-
.../adapters/AnimeSearchListAdapter.kt | 18 +-
.../repository/AnimeSearchRepositoryImpl.kt | 2 +-
.../anime_search/ui/AnimeSearchFragment.kt | 9 +-
.../adapters/AnimeSeasonalAdapter.kt | 18 +-
.../repository/AnimeSeasonalRepositoryImpl.kt | 2 +-
.../ui/AnimeSeasonalFragment.kt | 4 +-
.../adapters/AnimeSuggestionsAdapter.kt | 18 +-
.../AnimeSuggestionsRepositoryImpl.kt | 2 +-
.../ui/AnimeSuggestionsFragment.kt | 3 +-
.../auth/repository/OAuthRepository.kt | 4 +-
.../auth/repository/OAuthRepositoryImpl.kt | 32 +-
.../mediahub/modules/auth/ui/OAuthActivity.kt | 6 +-
.../modules/auth/vm/OAuthViewModel.kt | 4 +-
.../discover/adapters/AiringAnimeAdapter.kt | 18 +-
.../discover/adapters/AnimeRankingAdapter.kt | 18 +-
.../adapters/AnimeSuggestionsAdapter.kt | 18 +-
.../repository/DiscoverRepositoryImpl.kt | 69 +-
.../modules/discover/ui/DiscoverFragment.kt | 105 +--
.../mediahub/modules/main/ui/MainActivity.kt | 21 +-
.../manga/adapters/MangaListAdapter.kt | 34 +-
.../manga/repository/MangaRepositoryImpl.kt | 2 +-
.../modules/manga/ui/MangaFragment.kt | 2 +-
.../manga/ui/MangaListByStatusFragment.kt | 14 +-
.../modules/manga/vm/MangaViewModel.kt | 2 +-
.../adapters/RecommendedMangaAdapter.kt | 19 +-
.../adapters/RelatedAnimeAdapter.kt | 14 +-
.../adapters/RelatedMangaAdapter.kt | 14 +-
.../repository/MangaDetailsRepositoryImpl.kt | 73 +-
.../manga_details/ui/MangaDetailsFragment.kt | 632 +++++++++---------
.../manga_details/vm/MangaDetailsViewModel.kt | 3 +-
.../adapters/MangaRankingDetailedAdapter.kt | 22 +-
.../repository/MangaRankingRepositoryImpl.kt | 2 +-
.../manga_ranking/ui/MangaRankingFragment.kt | 41 +-
.../adapters/MangaSearchListAdapter.kt | 18 +-
.../repository/MangaSearchRepositoryImpl.kt | 2 +-
.../manga_search/ui/MangaSearchFragment.kt | 9 +-
.../repository/ProfileRepositoryImpl.kt | 25 +-
.../modules/profile/ui/ProfileFragment.kt | 314 ++++-----
.../repository/SettingsRepositoryImpl.kt | 3 +-
.../modules/settings/ui/SettingsFragment.kt | 40 +-
app/src/main/res/layout/activity_auth.xml | 6 +-
.../layout/anime_details_user_list_item.xml | 2 +-
app/src/main/res/layout/anime_list_item.xml | 6 +-
app/src/main/res/layout/content_profile.xml | 8 +-
.../layout/fragment_anime_list_by_status.xml | 2 +-
app/src/main/res/layout/manga_list_item.xml | 5 +-
.../main/res/layout/profile_details_item.xml | 2 +-
.../main/res/navigation/bottom_nav_graph.xml | 145 +---
app/src/main/res/values/colors.xml | 1 -
app/src/main/res/values/strings.xml | 178 ++++-
app/src/main/res/xml/root_preferences.xml | 2 +-
.../mediahub/AppAcceptedDeepLinkRegexTest.kt | 45 ++
.../mediahub/MALExternalLinksTest.kt | 2 +-
.../enum_tests/MangaRankingTypeTest.kt | 56 ++
.../mediahub/enum_tests/SortTypeEnumTest.kt | 25 -
.../PresentationExtensionsTest.kt | 162 -----
build.gradle | 8 +-
gradle.properties | 3 +-
122 files changed, 3207 insertions(+), 2356 deletions(-)
create mode 100644 app/src/androidTest/java/EnumTest.kt
create mode 100644 app/src/androidTest/java/PresentationExtensionsTest.kt
delete mode 100644 app/src/main/java/com/sharkaboi/mediahub/common/alarm_manager/NotifyAlarmManager.kt
delete mode 100644 app/src/main/java/com/sharkaboi/mediahub/common/constants/MALExternalLinks.kt
create mode 100644 app/src/main/java/com/sharkaboi/mediahub/common/constants/UIConstants.kt
rename app/src/main/java/com/sharkaboi/mediahub/data/api/{ => constants}/ApiConstants.kt (89%)
create mode 100644 app/src/main/java/com/sharkaboi/mediahub/data/api/constants/MALExternalLinks.kt
create mode 100644 app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeAiringStatus.kt
create mode 100644 app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeNsfwRating.kt
create mode 100644 app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeRating.kt
create mode 100644 app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaNsfwRating.kt
create mode 100644 app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaPublishingStatus.kt
delete mode 100644 app/src/main/java/com/sharkaboi/mediahub/data/wrappers/NoTokenFoundError.kt
delete mode 100644 app/src/main/java/com/sharkaboi/mediahub/di/AlarmManagerModule.kt
create mode 100644 app/src/test/java/com/sharkaboi/mediahub/AppAcceptedDeepLinkRegexTest.kt
create mode 100644 app/src/test/java/com/sharkaboi/mediahub/enum_tests/MangaRankingTypeTest.kt
delete mode 100644 app/src/test/java/com/sharkaboi/mediahub/enum_tests/SortTypeEnumTest.kt
delete mode 100644 app/src/test/java/com/sharkaboi/mediahub/extension_tests/PresentationExtensionsTest.kt
diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml
index 77e6cfd..340ac91 100644
--- a/.github/workflows/android.yaml
+++ b/.github/workflows/android.yaml
@@ -3,21 +3,102 @@ name: Android CI
on: [ push, pull_request ]
jobs:
- test:
- name: Run Unit Tests
+ build:
+ name: Gradle Build
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v1
+ - name: Checkout repo
+ uses: actions/checkout@v1
+
+ - uses: actions/cache@v2
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }}
+
- name: set up JDK 11
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v2
with:
- java-version: 11
+ java-version: '11'
+ distribution: 'adopt'
+
- name: Setup Gradle
run: chmod +x gradlew
+
- name: Gradle build
- run: bash ./gradlew build
+ run: bash ./gradlew assembleDebug
+
+ test:
+ name: Run Tests and lint checks
+ runs-on: macos-latest
+ timeout-minutes: 60
+ needs: build
+ strategy:
+ matrix:
+ api-level: [23, 26, 28]
+
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v2
+
+ - uses: actions/cache@v2
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }}
+
+ - uses: actions/cache@v2
+ id: avd-cache
+ with:
+ path: |
+ ~/.android/avd/*
+ ~/.android/adb*
+ key: avd-${{ matrix.api-level }}-default
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v2
+ with:
+ java-version: '11'
+ distribution: 'adopt'
+
+ - name: Setup Gradle
+ run: chmod +x gradlew
+
+ - name: Run kltlint
+ run: ./gradlew ktlintCheck
+
- name: Unit tests
- run: bash ./gradlew test --stacktrace
- - name: kltlint check
- run: bash ./gradlew ktlintCheck
\ No newline at end of file
+ run: ./gradlew test --stacktrace
+
+ - name: Create AVD snapshot
+ if: steps.avd-cache.outputs.cache-hit != 'true'
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: ${{ matrix.api-level }}
+ target: default
+ disable-animations: false
+ force-avd-creation: false
+ emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+ script: echo "Generated AVD snapshot."
+
+ - name: Instrumentation Tests
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: ${{ matrix.api-level }}
+ arch: x86
+ target: default
+ disable-animations: true
+ force-avd-creation: false
+ emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot-save
+ emulator-build: 7425822 # https://github.com/ReactiveCircus/android-emulator-runner/issues/160
+ script: ./gradlew connectedCheck --stacktrace
+
+ - name: Upload Reports
+ uses: actions/upload-artifact@v2
+ with:
+ name: Test-Reports
+ path: app/build/reports
+ if: always()
\ No newline at end of file
diff --git a/README.md b/README.md
index c0f935c..0e937fa 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,8 @@
MediaHub
-
-
+
+
@@ -25,6 +25,7 @@
## Uses
+
* [Kotlin](https://kotlinlang.org/)
* MVI/MVVM Architecture
* [Retrofit](https://square.github.io/retrofit/)
@@ -50,9 +51,11 @@
* [OSS licenses plugin](https://developers.google.com/android/guides/opensource)
## Releases
+
* Check out the latest releases [here](https://github.com/Sharkaboi/MediaHub/releases)
## Screenshots
+
Anime | Manga
:-------------------------:|:-------------------------:
![](assets/screenshots/anime.png) | ![](assets/screenshots/manga.png)
@@ -74,30 +77,45 @@ Settings | Update
![](assets/screenshots/settings.png) | ![](assets/screenshots/update.png)
## Build instructions
+
* Install Gradle and Kotlin.
* Clone project.
* Register your app with MyAnimeList as show [here](https://myanimelist.net/blog.php?eid=835707)
-* In the project root, add `clientId=` to the `secrets.properties` file. Create if not found.
-* Open in Android studio or Intellij and build and sync project (Be sure the generated classes of Hilt, ViewBinding and Apollo are generated).
+* In the project root, add `clientId=` to the `secrets.properties` file. Create if not
+ found.
+* Open in Android studio or Intellij and build and sync project (Be sure the generated classes of
+ Hilt, ViewBinding and Apollo are generated).
* Run on any device and perform OAuth login to give access to your account.
## Credits
+
* [Photo by Audrey Mari from Pexels](https://www.pexels.com/photo/photo-of-japanese-lanterns-3421920/)
* [Tabler icons by Paweł Kuna](https://tablericons.com/)
* [Bubbles icons by Umar Irshad](https://www.iconfinder.com/iconsets/48-bubbles)
## Contributing
+
PR's are welcome. Please try to follow the template.
## Privacy, Security and other info
-* App only acts as intermediate to MyAnimeList and AniList and does not have it's own server or store any data.
+
+* App only acts as intermediate to MyAnimeList and AniList and does not have it's own server or
+ store any data.
* App only has network permissions, the `WRITE_EXTERNAL_STORAGE`,
-`READ_EXTERNAL_STORAGE` & `FOREGROUND_SERVICE` in the merged manifest is from [LeakCanary](https://square.github.io/leakcanary/) which is not included in the release builds.
-* App stores the token using [Datastore](https://developer.android.com/topic/libraries/architecture/datastore), which doesn't have an encryption library yet.
-This implies anyone with debugging access or root access to your phone can get the your OAuth token. Please keep this in mind.
-* The app doesn't fetch "live data" and only shows the snapshot of data of when it was fetched. Please refresh to see any changes made through the MyAnimeList website or other clients.
+ `READ_EXTERNAL_STORAGE` & `FOREGROUND_SERVICE` in the merged manifest is
+ from [LeakCanary](https://square.github.io/leakcanary/) which is not included in the release
+ builds.
+* App stores the token
+ using [Datastore](https://developer.android.com/topic/libraries/architecture/datastore), which
+ is not encrypted. This implies anyone with debugging access or root access
+ to your phone can get the your OAuth token if needed. Please keep this in mind.
+* The app doesn't fetch "live data" and only shows the snapshot of data of when it was fetched.
+ Please refresh to see any changes made through the MyAnimeList website or other clients.
+* The app queries all browsers in the device to handle unsupported MyAnimeList deeplinks. This is
+ done as the API does not still support all the features provided in the web interface.
## Licence
+
```
MIT License
diff --git a/app/build.gradle b/app/build.gradle
index e3ff244..9f5d3d9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -11,14 +11,14 @@ plugins {
}
android {
- compileSdkVersion 30
+ compileSdkVersion 31
defaultConfig {
applicationId "com.sharkaboi.mediahub"
minSdkVersion 23
- targetSdkVersion 30
+ targetSdkVersion 31
versionCode 1
- versionName "1.2"
+ versionName "1.3"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -34,11 +34,11 @@ android {
}
compileOptions {
coreLibraryDesugaringEnabled true
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
- jvmTarget = '1.8'
+ jvmTarget = '11'
}
buildFeatures {
diff --git a/app/src/androidTest/java/EnumTest.kt b/app/src/androidTest/java/EnumTest.kt
new file mode 100644
index 0000000..9eb52e2
--- /dev/null
+++ b/app/src/androidTest/java/EnumTest.kt
@@ -0,0 +1,195 @@
+import androidx.test.platform.app.InstrumentationRegistry
+import com.sharkaboi.mediahub.data.api.enums.*
+import com.sharkaboi.mediahub.data.api.enums.AnimeRating.getAnimeRating
+import org.junit.Assert
+import org.junit.Test
+
+class EnumTest {
+
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+
+ @Test
+ fun formattedStringOfAnimeMustContainSameElementsAsEnumValues() {
+ Assert.assertEquals(
+ UserAnimeSortType.values().size,
+ UserAnimeSortType.getFormattedArray(context).size
+ )
+ }
+
+ @Test
+ fun formattedStringOfMangaMustContainSameElementsAsEnumValues() {
+ Assert.assertEquals(
+ UserMangaSortType.values().size,
+ UserMangaSortType.getFormattedArray(context).size
+ )
+ }
+
+ @Test
+ fun getAnimeAiringStatusWithValidStatusReturnsValidString() {
+ val inputToExpectedMap = mapOf(
+ "finished_airing" to "Finished airing",
+ "currently_airing" to "Currently airing",
+ "not_yet_aired" to "Yet to be aired",
+ "invalid_status" to "Airing status : N/A"
+ )
+ inputToExpectedMap.forEach { (inputString, expectedString) ->
+ Assert.assertEquals(context.getAnimeAiringStatus(inputString), expectedString)
+ }
+ }
+
+ @Test
+ fun getAnimeNsfwRatingWithRatingReturnsValidString() {
+ val inputToExpectedMap = mapOf(
+ "white" to "SFW",
+ "gray" to "NSFW?",
+ "black" to "NSFW",
+ null to "N/A",
+ "invalid_rating" to "SFW : N/A"
+ )
+ inputToExpectedMap.forEach { (inputString, expectedString) ->
+ Assert.assertEquals(context.getAnimeNsfwRating(inputString), expectedString)
+ }
+ }
+
+ @Test
+ fun getMangaNsfwRatingWithRatingReturnsValidString() {
+ val inputToExpectedMap = mapOf(
+ "white" to "Safe for work (SFW)",
+ "gray" to "Maybe not safe for work (NSFW)",
+ "black" to "Not safe for work (NSFW)",
+ null to "N/A",
+ "invalid_rating" to "N/A"
+ )
+ inputToExpectedMap.forEach { (inputString, expectedString) ->
+ Assert.assertEquals(context.getMangaNsfwRating(inputString), expectedString)
+ }
+ }
+
+ @Test
+ fun getMangaPublishStatusWithStatusReturnsValidString() {
+ val inputToExpectedMap = mapOf(
+ "finished" to "Finished publishing",
+ "currently_publishing" to "Currently publishing",
+ "not_yet_published" to "Yet to be published",
+ "invalid_rating" to "Publishing status : N/A"
+ )
+ inputToExpectedMap.forEach { (inputString, expectedString) ->
+ Assert.assertEquals(context.getMangaPublishStatus(inputString), expectedString)
+ }
+ }
+
+ @Test
+ fun getAnimeRankingWithRankingReturnsValidString() {
+ val inputToExpectedMap = mapOf(
+ AnimeRankingType.all to "All",
+ AnimeRankingType.airing to "Airing",
+ AnimeRankingType.upcoming to "Upcoming",
+ AnimeRankingType.tv to "TV",
+ AnimeRankingType.ova to "OVA",
+ AnimeRankingType.movie to "Movie",
+ AnimeRankingType.special to "Specials",
+ AnimeRankingType.bypopularity to "By popularity",
+ AnimeRankingType.favorite to "In your list",
+ )
+ inputToExpectedMap.forEach { (input, expectedString) ->
+ Assert.assertEquals(input.getAnimeRanking(context), expectedString)
+ }
+ }
+
+ @Test
+ fun getFormattedStringOfAnimeStatusWithValidStatusReturnsValidString() {
+ val inputToExpectedMap = mapOf(
+ AnimeStatus.all to "All",
+ AnimeStatus.watching to "Watching",
+ AnimeStatus.plan_to_watch to "Planned",
+ AnimeStatus.completed to "Completed",
+ AnimeStatus.on_hold to "On hold",
+ AnimeStatus.dropped to "Dropped"
+ )
+ inputToExpectedMap.forEach { (input, expectedString) ->
+ Assert.assertEquals(input.getFormattedString(context), expectedString)
+ }
+ }
+
+ @Test
+ fun getFormattedStringOfMangaStatusWithValidStatusReturnsValidString() {
+ val inputToExpectedMap = mapOf(
+ MangaStatus.all to "All",
+ MangaStatus.reading to "Reading",
+ MangaStatus.plan_to_read to "Planned",
+ MangaStatus.completed to "Completed",
+ MangaStatus.on_hold to "On hold",
+ MangaStatus.dropped to "Dropped"
+ )
+ inputToExpectedMap.forEach { (input, expectedString) ->
+ Assert.assertEquals(input.getFormattedString(context), expectedString)
+ }
+ }
+
+ @Test
+ fun getFormattedStringOfUserAnimeSortTypeWithValidTypeReturnsValidString() {
+ val inputToExpectedMap = mapOf(
+ MangaStatus.all to "All",
+ MangaStatus.reading to "Reading",
+ MangaStatus.plan_to_read to "Planned",
+ MangaStatus.completed to "Completed",
+ MangaStatus.on_hold to "On hold",
+ MangaStatus.dropped to "Dropped"
+ )
+ inputToExpectedMap.forEach { (input, expectedString) ->
+ Assert.assertEquals(input.getFormattedString(context), expectedString)
+ }
+ }
+
+ @Test
+ fun getFormattedStringOfMangaRankingWithValidRankingReturnsValidString() {
+ val inputToExpectedMap = mapOf(
+ MangaRankingType.all to "All",
+ MangaRankingType.manga to "Manga",
+ MangaRankingType.oneshots to "One-shots",
+ MangaRankingType.doujin to "Doujins",
+ MangaRankingType.lightnovels to "Light novels",
+ MangaRankingType.novels to "Novels",
+ MangaRankingType.manhwa to "Manhwa",
+ MangaRankingType.manhua to "Manhua",
+ MangaRankingType.bypopularity to "By popularity",
+ MangaRankingType.favorite to "In your list",
+ )
+ inputToExpectedMap.forEach { (input, expectedString) ->
+ Assert.assertEquals(input.getFormattedString(context), expectedString)
+ }
+ }
+
+ @Test
+ fun getAnimeRatingWithRatingReturnsValidString() {
+ val inputToExpectedMap = mapOf(
+ null to "N/A",
+ "g" to "G - All ages",
+ "pg" to "PG",
+ "pg_13" to "PG 13",
+ "r" to "R - 17+",
+ "r+" to "R+",
+ "rx" to "Rx - Hentai",
+ "invalid_rating" to "N/A",
+ )
+ inputToExpectedMap.forEach { (inputString, expectedString) ->
+ Assert.assertEquals(context.getAnimeRating(inputString), expectedString)
+ }
+ }
+
+ @Test
+ fun getFormattedArrayOfUserAnimeSortTypeHasSameSizeAsEnumValues() {
+ Assert.assertEquals(
+ UserAnimeSortType.getFormattedArray(context).size,
+ UserAnimeSortType.values().size
+ )
+ }
+
+ @Test
+ fun getFormattedArrayOfUserMangaSortTypeHasSameSizeAsEnumValues() {
+ Assert.assertEquals(
+ UserMangaSortType.getFormattedArray(context).size,
+ UserMangaSortType.values().size
+ )
+ }
+}
diff --git a/app/src/androidTest/java/PresentationExtensionsTest.kt b/app/src/androidTest/java/PresentationExtensionsTest.kt
new file mode 100644
index 0000000..32d8546
--- /dev/null
+++ b/app/src/androidTest/java/PresentationExtensionsTest.kt
@@ -0,0 +1,559 @@
+import androidx.core.text.toSpanned
+import androidx.test.platform.app.InstrumentationRegistry
+import com.sharkaboi.mediahub.common.extensions.*
+import com.sharkaboi.mediahub.data.api.models.anime.AnimeByIDResponse
+import com.sharkaboi.mediahub.data.api.models.manga.MangaByIDResponse
+import org.junit.Assert
+import org.junit.Test
+import kotlin.time.ExperimentalTime
+
+class PresentationExtensionsTest {
+
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+
+ @Test
+ fun getProgressStringWithNullProgressAndNullTotalReturnsValidString() {
+ val progress = null
+ val total = null
+ val expectedString = "0/??"
+ val resultString = context.getProgressStringWith(progress, total)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getProgressStringWithValidProgressAndNullTotalReturnsValidString() {
+ val progress = 5
+ val total = null
+ val expectedString = "5/??"
+ val resultString = context.getProgressStringWith(progress, total)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getProgressStringWithNullProgressAndValidTotalReturnsValidString() {
+ val progress = null
+ val total = 10
+ val expectedString = "0/10"
+ val resultString = context.getProgressStringWith(progress, total)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getProgressStringWith0ProgressAnd0TotalReturnsValidString() {
+ val progress = 0
+ val total = 0
+ val expectedString = "0/??"
+ val resultString = context.getProgressStringWith(progress, total)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getProgressStringWithNullProgressAnd0TotalReturnsValidString() {
+ val progress = null
+ val total = 0
+ val expectedString = "0/??"
+ val resultString = context.getProgressStringWith(progress, total)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getProgressStringWithValidProgressAndValidTotalReturnsValidString() {
+ val progress = 10
+ val total = 10
+ val expectedString = "10/10"
+ val resultString = context.getProgressStringWith(progress, total)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getEpisodesOfAnimeStringWithValidEpisodeCountReturnsValidString() {
+ val episodeCount = 10
+ val expectedString = "10 eps"
+ val resultString = context.getEpisodesOfAnimeString(episodeCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getEpisodesOfAnimeStringWith1EpisodeCountReturnsValidString() {
+ val episodeCount = 1
+ val expectedString = "1 ep"
+ val resultString = context.getEpisodesOfAnimeString(episodeCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getEpisodesOfAnimeStringWith0EpisodeCountReturnsValidString() {
+ val episodeCount = 0
+ val expectedString = "N/A eps"
+ val resultString = context.getEpisodesOfAnimeString(episodeCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getEpisodesOfAnimeFullStringWithValidEpisodeCountReturnsValidString() {
+ val episodeCount = 10.0
+ val expectedString = "10 episodes"
+ val resultString = context.getEpisodesOfAnimeFullString(episodeCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getEpisodesOfAnimeFullStringWith1EpisodeCountReturnsValidString() {
+ val episodeCount = 1.0
+ val expectedString = "1 episode"
+ val resultString = context.getEpisodesOfAnimeFullString(episodeCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getEpisodesOfAnimeFullStringWith0EpisodeCountReturnsValidString() {
+ val episodeCount = 0.0
+ val expectedString = "N/A episodes"
+ val resultString = context.getEpisodesOfAnimeFullString(episodeCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getDaysCountStringWithValidDaysCountReturnsValidString() {
+ val daysCount = 10L
+ val expectedString = "10 days"
+ val resultString = context.getDaysCountString(daysCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getDaysCountStringWith1DayCountReturnsValidString() {
+ val daysCount = 1L
+ val expectedString = "1 day"
+ val resultString = context.getDaysCountString(daysCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getDaysCountStringWith0DaysCountReturnsValidString() {
+ val daysCount = 0L
+ val expectedString = "N/A days"
+ val resultString = context.getDaysCountString(daysCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getVolumesOfMangaStringWithValidVolumeCountReturnsValidString() {
+ val volCount = 10
+ val expectedString = "10 vols"
+ val resultString = context.getVolumesOfMangaString(volCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getVolumesOfMangaStringWith1VolumeCountReturnsValidString() {
+ val volCount = 1
+ val expectedString = "1 vol"
+ val resultString = context.getVolumesOfMangaString(volCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getVolumesOfMangaStringWith0VolumeCountReturnsValidString() {
+ val volCount = 0
+ val expectedString = "N/A vols"
+ val resultString = context.getVolumesOfMangaString(volCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getChaptersOfMangaStringWithValidChapterCountReturnsValidString() {
+ val chapCount = 10
+ val expectedString = "10 chaps"
+ val resultString = context.getChaptersOfMangaString(chapCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getChaptersOfMangaStringWith1ChapterCountReturnsValidString() {
+ val volCount = 1
+ val expectedString = "1 chap"
+ val resultString = context.getChaptersOfMangaString(volCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getChaptersOfMangaStringWith0ChapterCountReturnsValidString() {
+ val volCount = 0
+ val expectedString = "N/A chaps"
+ val resultString = context.getChaptersOfMangaString(volCount)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getAnimeSeasonStringWithNullSeasonAndNullYearReturnsValidString() {
+ val season = null
+ val year = null
+ val expectedString = "Season : N/A"
+ val resultString = context.getAnimeSeasonString(season, year)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getAnimeSeasonStringWithNullSeasonAnd0YearReturnsValidString() {
+ val season = null
+ val year = 0
+ val expectedString = "Season : N/A"
+ val resultString = context.getAnimeSeasonString(season, year)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getAnimeSeasonStringWithValidSeasonAndNullYearReturnsValidString() {
+ val season = "Summer"
+ val year = null
+ val expectedString = "Season : N/A"
+ val resultString = context.getAnimeSeasonString(season, year)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getAnimeSeasonStringWithValidSeasonAndValidYearReturnsValidString() {
+ val season = "Summer"
+ val year = 2020
+ val expectedString = "Summer 2020"
+ val resultString = context.getAnimeSeasonString(season, year)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getAnimeOriginalSourceStringWithNullSourceReturnsValidString() {
+ val source = null
+ val expectedString = "From N/A"
+ val resultString = context.getAnimeOriginalSourceString(source)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getAnimeOriginalSourceStringWithLowerCaseSourceReturnsValidString() {
+ val source = "manga"
+ val expectedString = "From Manga"
+ val resultString = context.getAnimeOriginalSourceString(source)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getAnimeOriginalSourceStringWithUnderscoreSourceReturnsValidString() {
+ val source = "light_novel"
+ val expectedString = "From Light novel"
+ val resultString = context.getAnimeOriginalSourceString(source)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getMediaTypeStringWithWithValidMediaTypeReturnsValidString() {
+ val mediaType = "Movie"
+ val expectedString = "Type : MOVIE"
+ val resultString = context.getMediaTypeStringWith(mediaType)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getRatingStringWithRatingWithWithNullRatingReturnsValidString() {
+ val rating = null
+ val expectedString = "★ 0.0"
+ val resultString = context.getRatingStringWithRating(rating)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getRatingStringWithRatingWithWith0RatingReturnsValidString() {
+ val rating = 0
+ val expectedString = "★ 0.0"
+ val resultString = context.getRatingStringWithRating(rating)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getRatingStringWithRatingWithWithHalfRatingReturnsValidString() {
+ val rating = 0.5
+ val expectedString = "★ 0.5"
+ val resultString = context.getRatingStringWithRating(rating)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getRatingStringWithRatingWithWith1RatingReturnsValidString() {
+ val rating = 1
+ val expectedString = "★ 1"
+ val resultString = context.getRatingStringWithRating(rating)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getRatingStringWithRatingWithWithMore9RatingReturnsValidString() {
+ val rating = 9.999
+ val expectedString = "★ 10.0"
+ val resultString = context.getRatingStringWithRating(rating)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getRatingStringWithRatingWithWithLess9RatingReturnsValidString() {
+ val rating = 9.125
+ val expectedString = "★ 9.1"
+ val resultString = context.getRatingStringWithRating(rating)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getAnimeBroadcastTimeWithNullTimeReturnsValidString() {
+ val time = null
+ val expectedString = "N/A"
+ val resultString = context.getAnimeBroadcastTime(time)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getAnimeBroadcastTimeWithNullTimeAndValidDayReturnsValidString() {
+ val time = AnimeByIDResponse.Broadcast(
+ startTime = null,
+ dayOfTheWeek = "sunday"
+ )
+ val expectedString = "On sunday"
+ val resultString = context.getAnimeBroadcastTime(time)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getFormattedAnimeTitlesStringWithNullReturnsValidString() {
+ val titles = null
+ val expectedString = "N/A".toSpanned()
+ val resultString = context.getFormattedAnimeTitlesString(titles)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getFormattedAnimeTitlesStringWithNullTitlesReturnsValidString() {
+ val titles = AnimeByIDResponse.AlternativeTitles(
+ en = null,
+ ja = null,
+ synonyms = null
+ )
+ val resultString = context.getFormattedAnimeTitlesString(titles)
+ Assert.assertTrue(resultString.split("N/A").size == 4)
+ }
+
+ @Test
+ fun getFormattedAnimeTitlesStringWithNullTitlesAndNoSynonymsReturnsValidString() {
+ val titles = AnimeByIDResponse.AlternativeTitles(
+ en = null,
+ ja = null,
+ synonyms = emptyList()
+ )
+ val resultString = context.getFormattedAnimeTitlesString(titles)
+ Assert.assertTrue(resultString.split("N/A").size == 4)
+ }
+
+ @Test
+ fun getFormattedAnimeTitlesStringWithBlankTitlesReturnsValidString() {
+ val titles = AnimeByIDResponse.AlternativeTitles(
+ en = " ",
+ ja = " ",
+ synonyms = emptyList()
+ )
+ val resultString = context.getFormattedAnimeTitlesString(titles)
+ Assert.assertTrue(resultString.split("N/A").size == 4)
+ }
+
+ @Test
+ fun getFormattedAnimeTitlesStringWithValidTitlesReturnsValidString() {
+ val titles = AnimeByIDResponse.AlternativeTitles(
+ en = "english",
+ ja = "japanese",
+ synonyms = listOf("synonyms")
+ )
+ val resultString = context.getFormattedAnimeTitlesString(titles)
+ Assert.assertFalse(resultString.contains("N/A"))
+ }
+
+ @Test
+ fun getFormattedMangaTitlesStringWithNullReturnsValidString() {
+ val titles = null
+ val expectedString = "N/A".toSpanned()
+ val resultString = context.getFormattedMangaTitlesString(titles)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getFormattedMangaTitlesStringWithNullTitlesReturnsValidString() {
+ val titles = MangaByIDResponse.AlternativeTitles(
+ en = null,
+ ja = null,
+ synonyms = null
+ )
+ val resultString = context.getFormattedMangaTitlesString(titles)
+ Assert.assertTrue(resultString.split("N/A").size == 4)
+ }
+
+ @Test
+ fun getFormattedMangaTitlesStringWithNullTitlesAndNoSynonymsReturnsValidString() {
+ val titles = MangaByIDResponse.AlternativeTitles(
+ en = null,
+ ja = null,
+ synonyms = emptyList()
+ )
+ val resultString = context.getFormattedMangaTitlesString(titles)
+ Assert.assertTrue(resultString.split("N/A").size == 4)
+ }
+
+ @Test
+ fun getFormattedMangaTitlesStringWithBlankTitlesReturnsValidString() {
+ val titles = MangaByIDResponse.AlternativeTitles(
+ en = " ",
+ ja = " ",
+ synonyms = emptyList()
+ )
+ val resultString = context.getFormattedMangaTitlesString(titles)
+ Assert.assertTrue(resultString.split("N/A").size == 4)
+ }
+
+ @Test
+ fun getFormattedMangaTitlesStringWithValidTitlesReturnsValidString() {
+ val titles = MangaByIDResponse.AlternativeTitles(
+ en = "english",
+ ja = "japanese",
+ synonyms = listOf("synonyms")
+ )
+ val resultString = context.getFormattedMangaTitlesString(titles)
+ Assert.assertFalse(resultString.contains("N/A"))
+ }
+
+ @Test
+ fun getAnimeStatsWithNullStatsReturnsValidString() {
+ val stats = null
+ val expectedString = "N/A".toSpanned()
+ val resultString = context.getAnimeStats(stats)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @Test
+ fun getAnimeStatsWithValidStatsReturnsValidString() {
+ val stats = AnimeByIDResponse.Statistics(
+ numListUsers = 0,
+ status = AnimeByIDResponse.Statistics.Status(
+ watching = "0",
+ completed = "0",
+ onHold = "0",
+ dropped = "0",
+ planToWatch = "0",
+ )
+ )
+ val resultString = context.getAnimeStats(stats)
+ Assert.assertTrue(resultString.isNotBlank())
+ }
+
+ @Test
+ fun getMangaStatsWithNullStatsReturnsValidString() {
+ val numListUsers = null
+ val numScoredUsers = null
+ val resultString = context.getMangaStats(numListUsers, numScoredUsers)
+ Assert.assertTrue(resultString.split("N/A").size == 3)
+ }
+
+ @Test
+ fun getMangaStatsWithValidStatsReturnsValidString() {
+ val numListUsers = 0
+ val numScoredUsers = 0
+ val resultString = context.getMangaStats(numListUsers, numScoredUsers)
+ Assert.assertFalse(resultString.contains("N/A"))
+ }
+
+ @ExperimentalTime
+ @Test
+ fun getEpisodeLengthFromSecondsWithNullSecondsReturnsValidString() {
+ val seconds = null
+ val expectedString = "N/A per ep"
+ val resultString = context.getEpisodeLengthFromSeconds(seconds)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @ExperimentalTime
+ @Test
+ fun getEpisodeLengthFromSecondsWith0SecondsReturnsValidString() {
+ val seconds = 0
+ val expectedString = "N/A per ep"
+ val resultString = context.getEpisodeLengthFromSeconds(seconds)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @ExperimentalTime
+ @Test
+ fun getEpisodeLengthFromSecondsWithNegativeSecondsReturnsValidString() {
+ val seconds = -1
+ val expectedString = "N/A per ep"
+ val resultString = context.getEpisodeLengthFromSeconds(seconds)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @ExperimentalTime
+ @Test
+ fun getEpisodeLengthFromSecondsWithValidHourSecondsReturnsValidString() {
+ val seconds = 90 * 60
+ val expectedString = "1h 30m per ep"
+ val resultString = context.getEpisodeLengthFromSeconds(seconds)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @ExperimentalTime
+ @Test
+ fun getEpisodeLengthFromSecondsWithValidMinutesSecondsReturnsValidString() {
+ val seconds = 30 * 60
+ val expectedString = "30m per ep"
+ val resultString = context.getEpisodeLengthFromSeconds(seconds)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @ExperimentalTime
+ @Test
+ fun getAiringTimeFormattedWith0TimeReturnsValidString() {
+ val time = GetNextAiringAnimeEpisodeQuery.NextAiringEpisode(
+ timeUntilAiring = 0,
+ episode = 7
+ )
+ val expectedString = "Episode 7 airs in 0h 0m 0s"
+ val resultString = context.getAiringTimeFormatted(time)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @ExperimentalTime
+ @Test
+ fun getAiringTimeFormattedWithMinutesTimeReturnsValidString() {
+ val time = GetNextAiringAnimeEpisodeQuery.NextAiringEpisode(
+ timeUntilAiring = 30 * 60,
+ episode = 7
+ )
+ val expectedString = "Episode 7 airs in 30m"
+ val resultString = context.getAiringTimeFormatted(time)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @ExperimentalTime
+ @Test
+ fun getAiringTimeFormattedWithHoursTimeReturnsValidString() {
+ val time = GetNextAiringAnimeEpisodeQuery.NextAiringEpisode(
+ timeUntilAiring = 90 * 60,
+ episode = 7
+ )
+ val expectedString = "Episode 7 airs in 1h 30m"
+ val resultString = context.getAiringTimeFormatted(time)
+ Assert.assertEquals(expectedString, resultString)
+ }
+
+ @ExperimentalTime
+ @Test
+ fun getAiringTimeFormattedWithDaysTimeReturnsValidString() {
+ val time = GetNextAiringAnimeEpisodeQuery.NextAiringEpisode(
+ timeUntilAiring = 90 * 24 * 60 * 60,
+ episode = 7
+ )
+ val expectedString = "Episode 7 airs in 90d 0h 0m"
+ val resultString = context.getAiringTimeFormatted(time)
+ Assert.assertEquals(expectedString, resultString)
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 414da35..da2d8e2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -66,4 +66,10 @@
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
android:theme="@style/Theme.MediaHub.ActionBar" />
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/graphql/nextAiringAnimeEpisode.graphql b/app/src/main/graphql/nextAiringAnimeEpisode.graphql
index fd4f0dc..206968b 100644
--- a/app/src/main/graphql/nextAiringAnimeEpisode.graphql
+++ b/app/src/main/graphql/nextAiringAnimeEpisode.graphql
@@ -2,13 +2,9 @@ query GetNextAiringAnimeEpisode(
$idMal: Int!
) {
Media(idMal: $idMal, type: ANIME) {
- id
- idMal
nextAiringEpisode {
- id
episode
timeUntilAiring
- airingAt
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/sharkaboi/mediahub/common/alarm_manager/NotifyAlarmManager.kt b/app/src/main/java/com/sharkaboi/mediahub/common/alarm_manager/NotifyAlarmManager.kt
deleted file mode 100644
index 9716242..0000000
--- a/app/src/main/java/com/sharkaboi/mediahub/common/alarm_manager/NotifyAlarmManager.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.sharkaboi.mediahub.common.alarm_manager
-
-import android.app.AlarmManager
-import android.content.Context
-
-class NotifyAlarmManager(context: Context) {
-
- init {
- val alarmManager =
- context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
- }
-}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/common/constants/AppConstants.kt b/app/src/main/java/com/sharkaboi/mediahub/common/constants/AppConstants.kt
index ed0a25f..6bfa712 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/common/constants/AppConstants.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/common/constants/AppConstants.kt
@@ -1,11 +1,10 @@
package com.sharkaboi.mediahub.common.constants
-class AppConstants {
- companion object {
- const val githubLink = "https://github.com/Sharkaboi/MediaHub"
- const val githubUsername = "Sharkaboi"
- const val githubRepoName = "MediaHub"
- const val description = "Simple unofficial MyAnimeList client."
- const val oAuthDeepLinkUri = "mediahub://callback"
- }
+object AppConstants {
+ const val githubLink = "https://github.com/Sharkaboi/MediaHub"
+ const val githubUsername = "Sharkaboi"
+ const val githubRepoName = "MediaHub"
+ const val description = "Simple, unofficial MyAnimeList client."
+ const val oAuthDeepLinkUri = "mediahub://callback"
+ const val nonDeepLinkedUrl = "https://sharkaboi.github.io"
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/common/constants/MALExternalLinks.kt b/app/src/main/java/com/sharkaboi/mediahub/common/constants/MALExternalLinks.kt
deleted file mode 100644
index 6f8124d..0000000
--- a/app/src/main/java/com/sharkaboi/mediahub/common/constants/MALExternalLinks.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-package com.sharkaboi.mediahub.common.constants
-
-import com.sharkaboi.mediahub.common.extensions.replaceWhiteSpaceWithUnderScore
-import com.sharkaboi.mediahub.data.api.models.anime.AnimeByIDResponse
-import com.sharkaboi.mediahub.data.api.models.manga.MangaByIDResponse
-
-class MALExternalLinks {
- companion object {
- fun getAnimeGenresLink(genre: AnimeByIDResponse.Genre): String {
- return "https://myanimelist.net/anime/genre" +
- "/${genre.id}/${genre.name.replaceWhiteSpaceWithUnderScore()}"
- }
-
- fun getAnimeCharactersLink(id: Int, title: String): String {
- return "https://myanimelist.net/anime" +
- "/$id/${title.replaceWhiteSpaceWithUnderScore()}/characters"
- }
-
- fun getAnimeStaffLink(id: Int, title: String): String {
- return "https://myanimelist.net/anime" +
- "/$id/${title.replaceWhiteSpaceWithUnderScore()}/characters#staff"
- }
-
- fun getAnimeReviewsLink(id: Int, title: String): String {
- return "https://myanimelist.net/anime" +
- "/$id/${title.replaceWhiteSpaceWithUnderScore()}/reviews"
- }
-
- fun getAnimeNewsLink(id: Int, title: String): String {
- return "https://myanimelist.net/anime" +
- "/$id/${title.replaceWhiteSpaceWithUnderScore()}/news"
- }
-
- fun getAnimeVideosLink(id: Int, title: String): String {
- return "https://myanimelist.net/anime" +
- "/$id/${title.replaceWhiteSpaceWithUnderScore()}/video"
- }
-
- fun getAnimeProducerPageLink(studio: AnimeByIDResponse.Studio): String {
- return "https://myanimelist.net/anime/producer" +
- "/${studio.id}/${studio.name.replaceWhiteSpaceWithUnderScore()}"
- }
-
- fun getMangaAuthorPageLink(author: MangaByIDResponse.Author): String {
- val name =
- "${author.node.firstName} ${author.node.lastName}"
- .replaceWhiteSpaceWithUnderScore()
- return "https://myanimelist.net/people" +
- "/${author.node.id}/$name"
- }
-
- fun getMangaGenresLink(genre: MangaByIDResponse.Genre): String {
- return "https://myanimelist.net/manga/genre" +
- "/${genre.id}/${genre.name.replaceWhiteSpaceWithUnderScore()}"
- }
-
- fun getMangaCharactersLink(id: Int, title: String): String {
- return "https://myanimelist.net/manga" +
- "/$id/${title.replaceWhiteSpaceWithUnderScore()}/characters"
- }
-
- fun getMangaReviewsLink(id: Int, title: String): String {
- return "https://myanimelist.net/manga" +
- "/$id/${title.replaceWhiteSpaceWithUnderScore()}/reviews"
- }
-
- fun getMangaNewsLink(id: Int, title: String): String {
- return "https://myanimelist.net/manga" +
- "/$id/${title.replaceWhiteSpaceWithUnderScore()}/news"
- }
-
- fun getMangaSerializationPageLink(magazine: MangaByIDResponse.Serialization): String {
- return "https://myanimelist.net/manga/magazine" +
- "/${magazine.node.id}/${magazine.node.name.replaceWhiteSpaceWithUnderScore()}"
- }
-
- fun getBlogsLink(name: String): String {
- return "https://myanimelist.net/blog/$name"
- }
-
- fun getClubsLink(name: String): String {
- return "https://myanimelist.net/profile/$name/clubs"
- }
-
- fun getForumTopicsLink(name: String): String {
- return "https://myanimelist.net/forum/search?u=$name&q=&uloc=1&loc=-1"
- }
-
- fun getFriendsLink(name: String): String {
- return "https://myanimelist.net/profile/$name/friends"
- }
-
- fun getHistoryLink(name: String): String {
- return "https://myanimelist.net/history/$name"
- }
-
- fun getRecommendationsLink(name: String): String {
- return "https://myanimelist.net/profile/$name/recommendations"
- }
-
- fun getReviewsLink(name: String): String {
- return "https://myanimelist.net/profile/$name/reviews"
- }
- }
-}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/common/constants/UIConstants.kt b/app/src/main/java/com/sharkaboi/mediahub/common/constants/UIConstants.kt
new file mode 100644
index 0000000..c694055
--- /dev/null
+++ b/app/src/main/java/com/sharkaboi/mediahub/common/constants/UIConstants.kt
@@ -0,0 +1,52 @@
+package com.sharkaboi.mediahub.common.constants
+
+import coil.request.ImageRequest
+import coil.transform.RoundedCornersTransformation
+import com.google.android.material.chip.Chip
+import com.google.android.material.shape.ShapeAppearanceModel
+import com.sharkaboi.mediahub.R
+
+object UIConstants {
+ const val AnimeAndMangaGridSpanCount = 3
+ private const val AnimeAndMangaImageCornerRadius = 8f
+ private const val ProfileImageCornerRadius = 10f
+ private val ChipShapeAppearanceModel = ShapeAppearanceModel().withCornerSize(8f)
+ val AnimeImageBuilder: ImageRequest.Builder.() -> Unit = {
+ crossfade(true)
+ placeholder(R.drawable.ic_anime_placeholder)
+ error(R.drawable.ic_anime_placeholder)
+ fallback(R.drawable.ic_anime_placeholder)
+ transformations(
+ RoundedCornersTransformation(
+ topLeft = AnimeAndMangaImageCornerRadius,
+ topRight = AnimeAndMangaImageCornerRadius
+ )
+ )
+ }
+ val MangaImageBuilder: ImageRequest.Builder.() -> Unit = {
+ crossfade(true)
+ placeholder(R.drawable.ic_manga_placeholder)
+ error(R.drawable.ic_manga_placeholder)
+ fallback(R.drawable.ic_manga_placeholder)
+ transformations(
+ RoundedCornersTransformation(
+ topLeft = AnimeAndMangaImageCornerRadius,
+ topRight = AnimeAndMangaImageCornerRadius
+ )
+ )
+ }
+ val ProfileImageBuilder: ImageRequest.Builder.() -> Unit = {
+ crossfade(true)
+ placeholder(R.drawable.ic_profile_placeholder)
+ error(R.drawable.ic_profile_placeholder)
+ fallback(R.drawable.ic_profile_placeholder)
+ transformations(RoundedCornersTransformation(ProfileImageCornerRadius))
+ }
+
+ fun Chip.setMediaHubChipStyle(): Chip {
+ return this.apply {
+ setEnsureMinTouchTargetSize(false)
+ shapeAppearanceModel = ChipShapeAppearanceModel
+ }
+ }
+}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/common/extensions/KotlinResultExtension.kt b/app/src/main/java/com/sharkaboi/mediahub/common/extensions/KotlinResultExtension.kt
index cc5c4d3..d737773 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/common/extensions/KotlinResultExtension.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/common/extensions/KotlinResultExtension.kt
@@ -2,7 +2,10 @@ package com.sharkaboi.mediahub.common.extensions
internal fun Result.getOrNullWithStackTrace(): T? {
return when {
- isFailure -> null
+ isFailure -> {
+ exceptionOrNull()?.printStackTrace()
+ null
+ }
else -> this.getOrDefault(null)
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/common/extensions/PresentationExtensions.kt b/app/src/main/java/com/sharkaboi/mediahub/common/extensions/PresentationExtensions.kt
index 130f41d..8b771f6 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/common/extensions/PresentationExtensions.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/common/extensions/PresentationExtensions.kt
@@ -1,356 +1,228 @@
package com.sharkaboi.mediahub.common.extensions
-import android.text.Html
+import GetNextAiringAnimeEpisodeQuery
+import android.content.Context
import android.text.Spanned
+import androidx.annotation.StringRes
+import androidx.core.text.HtmlCompat
+import androidx.core.text.toSpanned
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.sharkaboi.mediahub.R
import com.sharkaboi.mediahub.common.util.getLocalDateFromDayAndTime
import com.sharkaboi.mediahub.data.api.models.anime.AnimeByIDResponse
import com.sharkaboi.mediahub.data.api.models.manga.MangaByIDResponse
-import java.text.DecimalFormat
import java.time.format.DateTimeFormatter
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
-internal fun Double.roundOfString(): String {
- if (this == 0.0) {
- return "0"
- }
- val format = DecimalFormat("#.##")
- return format.format(this)
+internal fun Context.getProgressStringWith(progress: Int?, total: Int?): String {
+ val totalCountString = if (total == null || total == 0)
+ getString(R.string.no_count_fount_marker)
+ else
+ total.toString()
+ val progressCount = progress ?: 0
+ return getString(
+ R.string.media_progress_template,
+ progressCount,
+ totalCountString
+ )
}
-internal fun String.getAnimeNsfwRating(): String {
- return when {
- this.trim() == "white" -> {
- "SFW"
- }
- this.trim() == "gray" -> {
- "NSFW?"
- }
- this.trim() == "black" -> {
- "NSFW"
- }
- else -> {
- "SFW : N/A"
- }
- }
+internal fun Context.getEpisodesOfAnimeString(episodes: Int): String {
+ return resources.getQuantityString(
+ R.plurals.episode_count_template,
+ episodes,
+ if (episodes == 0) getString(R.string.n_a) else episodes.toString()
+ )
}
-internal fun String.getMangaNsfwRating(): String {
- return when {
- this.trim() == "white" -> {
- "Safe for work (SFW)"
- }
- this.trim() == "gray" -> {
- "Maybe not safe for work (NSFW)"
- }
- this.trim() == "black" -> {
- "Not safe for work (NSFW)"
- }
- else -> {
- "N/A"
- }
- }
+internal fun Context.getEpisodesOfAnimeFullString(episodes: Double): String {
+ return resources.getQuantityString(
+ R.plurals.episode_count_full_template,
+ episodes.toInt(),
+ if (episodes == 0.0) getString(R.string.n_a) else episodes.toInt().toString()
+ )
}
-internal fun String.getRating(): String {
- return when {
- this.trim() == "g" -> {
- "G - All ages"
- }
- this.trim() == "pg" -> {
- "PG"
- }
- this.trim() == "pg_13" -> {
- "PG 13"
- }
- this.trim() == "r" -> {
- "R - 17+"
- }
- this.trim() == "r+" -> {
- "R+"
- }
- this.trim() == "rx" -> {
- "Rx - Hentai"
- }
- else -> {
- "N/A"
- }
- }
+internal fun Context.getDaysCountString(days: Long): String {
+ return resources.getQuantityString(
+ R.plurals.days_count_template,
+ days.toInt(),
+ if (days == 0L) getString(R.string.n_a) else days.toString()
+ )
}
-internal fun String.getAnimeAiringStatus(): String {
- return when {
- this.trim() == "finished_airing" -> {
- "Finished airing"
- }
- this.trim() == "currently_airing" -> {
- "Currently airing"
- }
- this.trim() == "not_yet_aired" -> {
- "Yet to be aired"
- }
- else -> {
- "Airing status : N/A"
- }
- }
+internal fun Context.getVolumesOfMangaString(volumes: Int): String {
+ return resources.getQuantityString(
+ R.plurals.volume_count_template,
+ volumes,
+ if (volumes == 0) getString(R.string.n_a) else volumes.toString()
+ )
}
-internal fun String.getMangaPublishStatus(): String {
- return when {
- this.trim() == "finished" -> {
- "Finished publishing"
- }
- this.trim() == "currently_publishing" -> {
- "Currently publishing"
- }
- this.trim() == "not_yet_published" -> {
- "Yet to be published"
- }
- else -> {
- "Publishing status : N/A"
- }
+internal fun Context.getChaptersOfMangaString(chapters: Int): String {
+ return resources.getQuantityString(
+ R.plurals.chapter_count_template,
+ chapters,
+ if (chapters == 0) getString(R.string.n_a) else chapters.toString()
+ )
+}
+
+internal fun Context.getAnimeSeasonString(season: String?, year: Int?): String {
+ return if (season == null || year == null) {
+ getString(R.string.anime_season_unknown_template, getString(R.string.n_a))
+ } else {
+ getString(R.string.anime_season_template, season.capitalizeFirst(), year)
}
}
-internal fun AnimeByIDResponse.Broadcast.getBroadcastTime(): String {
- try {
- if (this.startTime == null) {
- return "On ${this.dayOfTheWeek}"
- }
- val localTime = getLocalDateFromDayAndTime(this.dayOfTheWeek, this.startTime)
- return localTime?.format(DateTimeFormatter.ofPattern("EEEE h:mm a zzzz")) ?: "N/A"
- } catch (e: Exception) {
- e.printStackTrace()
- return "N/A"
+internal fun Context.getAnimeOriginalSourceString(source: String?): String {
+ val sourceValue = source?.replaceUnderScoreWithWhiteSpace()
+ ?.capitalizeFirst()
+ ?: getString(R.string.n_a)
+ return getString(R.string.anime_original_source_template, sourceValue)
+}
+
+internal fun Context.getMediaTypeStringWith(type: String): String {
+ return getString(
+ R.string.media_type_template,
+ type.uppercase()
+ )
+}
+
+internal fun Context.getRatingStringWithRating(rating: Number?): String {
+ return resources.getQuantityString(
+ R.plurals.rating_hint,
+ rating?.toInt() ?: 0,
+ rating?.toFloat() ?: 0F
+ )
+}
+
+internal fun Context.getAnimeBroadcastTime(broadcast: AnimeByIDResponse.Broadcast?): String {
+ runCatching {
+ if (broadcast == null) {
+ return getString(R.string.n_a)
+ } else if (broadcast.startTime == null) {
+ return getString(R.string.anime_broadcast_on_day, broadcast.dayOfTheWeek)
+ }
+ val localTime = getLocalDateFromDayAndTime(broadcast.dayOfTheWeek, broadcast.startTime)
+ return localTime?.format(DateTimeFormatter.ofPattern("EEEE h:mm a zzzz"))
+ ?: getString(R.string.n_a)
+ }.getOrElse {
+ it.printStackTrace()
+ return getString(R.string.n_a)
}
}
-internal fun AnimeByIDResponse.AlternativeTitles.getFormattedString(): Spanned {
- return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
- Html.fromHtml(
- """
- English title : ${
- this.en?.let {
- if (it.isBlank()) {
- "N/A"
- } else {
- it
- }
- } ?: "N/A"
- }
- Japanese title : ${
- this.ja?.let {
- if (it.isBlank()) {
- "N/A"
- } else {
- it
- }
- } ?: "N/A"
- }
- Synonyms : ${
- this.synonyms?.let {
- if (it.isEmpty()) {
- "N/A"
- } else {
- it.joinToString().ifBlank { "N/A" }
- }
- } ?: "N/A"
- }
- """.trimIndent(),
- Html.FROM_HTML_MODE_COMPACT
- )
- } else {
- Html.fromHtml(
- """
- English title : ${
- this.en?.let {
- if (it.isBlank()) {
- "N/A"
- } else {
- it
- }
- } ?: "N/A"
- }
- Japanese title : ${
- this.ja?.let {
- if (it.isBlank()) {
- "N/A"
- } else {
- it
- }
- } ?: "N/A"
- }
- Synonyms : ${
- this.synonyms?.let {
- if (it.isEmpty()) {
- "N/A"
- } else {
- it.joinToString().ifBlank { "N/A" }
- }
- } ?: "N/A"
- }
- """.trimIndent()
- )
+internal fun Context.getFormattedAnimeTitlesString(titles: AnimeByIDResponse.AlternativeTitles?): Spanned {
+ if (titles == null) {
+ return getString(R.string.n_a).toSpanned()
}
+ val synonyms = titles.synonyms?.joinToString().ifNullOrBlank { getString(R.string.n_a) }
+ val englishTitle = titles.en.ifNullOrBlank { getString(R.string.n_a) }
+ val japaneseTitle = titles.ja.ifNullOrBlank { getString(R.string.n_a) }
+ val html = getString(R.string.alternate_titles_html, englishTitle, japaneseTitle, synonyms)
+ return HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
}
-internal fun MangaByIDResponse.AlternativeTitles.getFormattedString(): Spanned {
- return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
- Html.fromHtml(
- """
- English title : ${
- this.en?.let {
- if (it.isBlank()) {
- "N/A"
- } else {
- it
- }
- } ?: "N/A"
- }
- Japanese title : ${
- this.ja?.let {
- if (it.isBlank()) {
- "N/A"
- } else {
- it
- }
- } ?: "N/A"
- }
- Synonyms : ${
- this.synonyms?.let {
- if (it.isEmpty()) {
- "N/A"
- } else {
- it.joinToString().ifBlank { "N/A" }
- }
- } ?: "N/A"
- }
- """.trimIndent(),
- Html.FROM_HTML_MODE_COMPACT
- )
- } else {
- Html.fromHtml(
- """
- English title : ${
- this.en?.let {
- if (it.isBlank()) {
- "N/A"
- } else {
- it
- }
- } ?: "N/A"
- }
- Japanese title : ${
- this.ja?.let {
- if (it.isBlank()) {
- "N/A"
- } else {
- it
- }
- } ?: "N/A"
- }
- Synonyms : ${
- this.synonyms?.let {
- if (it.isEmpty()) {
- "N/A"
- } else {
- it.joinToString().ifBlank { "N/A" }
- }
- } ?: "N/A"
- }
- """.trimIndent()
- )
+internal fun Context.getFormattedMangaTitlesString(titles: MangaByIDResponse.AlternativeTitles?): Spanned {
+ if (titles == null) {
+ return getString(R.string.n_a).toSpanned()
}
+ val synonyms = titles.synonyms?.joinToString().ifNullOrBlank { getString(R.string.n_a) }
+ val englishTitle = titles.en.ifNullOrBlank { getString(R.string.n_a) }
+ val japaneseTitle = titles.ja.ifNullOrBlank { getString(R.string.n_a) }
+ val html = getString(R.string.alternate_titles_html, englishTitle, japaneseTitle, synonyms)
+ return HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
}
-internal fun AnimeByIDResponse.Statistics.getStats(): Spanned {
- return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
- Html.fromHtml(
- """
- Number of users with this anime in list : ${
- this.numListUsers
- }
- Watching count : ${
- this.status.watching
- }
- Planned count : ${
- this.status.planToWatch
- }
- Completed count : ${
- this.status.completed
- }
- Dropped count : ${
- this.status.dropped
- }
- On hold count : ${
- this.status.onHold
- }
- """.trimIndent(),
- Html.FROM_HTML_MODE_COMPACT
- )
- } else {
- Html.fromHtml(
- """
- Number of users with this anime in list : ${
- this.numListUsers
- }
- Watching count : ${
- this.status.watching
- }
- Planned count : ${
- this.status.planToWatch
- }
- Completed count : ${
- this.status.completed
- }
- Dropped count : ${
- this.status.dropped
- }
- On hold count : ${
- this.status.onHold
- }
- """.trimIndent()
- )
+internal fun Context.getAnimeStats(stats: AnimeByIDResponse.Statistics?): Spanned {
+ if (stats == null) {
+ return getString(R.string.n_a).toSpanned()
}
+ val html = getString(
+ R.string.anime_stats_html,
+ stats.numListUsers.toLong(),
+ stats.status.watching.toLong(),
+ stats.status.planToWatch.toLong(),
+ stats.status.completed.toLong(),
+ stats.status.dropped.toLong(),
+ stats.status.onHold.toLong()
+ )
+ return HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
+}
+
+internal fun Context.getMangaStats(numListUsers: Int?, numScoredUsers: Int?): Spanned {
+ val listUsers = numListUsers?.toString() ?: getString(R.string.n_a)
+ val scoredUsers = numScoredUsers?.toString() ?: getString(R.string.n_a)
+ val html = getString(
+ R.string.manga_stats_html,
+ listUsers,
+ scoredUsers
+ )
+ return HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
}
@ExperimentalTime
-internal fun Int?.getEpisodeLengthFromSeconds(): String {
- try {
- if (this == null || this <= 0) {
- return "N/A per ep"
+internal fun Context.getEpisodeLengthFromSeconds(seconds: Int?): String {
+ runCatching {
+ if (seconds == null || seconds <= 0) {
+ return getString(R.string.anime_episode_length_n_a)
}
- val duration = Duration.seconds(this.toLong())
+ val duration = Duration.seconds(seconds.toLong())
val hours = duration.inWholeHours.toInt()
- val minutes = duration.minus(Duration.Companion.hours(hours)).inWholeMinutes
- return if (hours <= 0) "${minutes}m per ep" else "${hours}h ${minutes}m per ep"
- } catch (e: Exception) {
- e.printStackTrace()
- return "N/A per ep"
+ val minutes = duration.minus(Duration.hours(hours)).inWholeMinutes
+ return if (hours <= 0) getString(
+ R.string.anime_episode_length_mins,
+ minutes
+ ) else getString(
+ R.string.anime_episode_length_hours,
+ hours,
+ minutes
+ )
+ }.getOrElse {
+ it.printStackTrace()
+ return getString(R.string.anime_episode_length_n_a)
}
}
@ExperimentalTime
-internal fun Int.getAiringTimeFormatted(): String {
- try {
- if (this <= 0) {
- return "0h 0m 0s"
+internal fun Context.getAiringTimeFormatted(nextEp: GetNextAiringAnimeEpisodeQuery.NextAiringEpisode): String {
+ val timeFromNow = runCatching {
+ if (nextEp.timeUntilAiring <= 0) {
+ return@runCatching getString(R.string.anime_next_episode_airing_n_a)
}
- var currentDuration = Duration.seconds(this.toLong())
+ var currentDuration = Duration.seconds(nextEp.timeUntilAiring.toLong())
val days = currentDuration.inWholeDays.toInt()
currentDuration = currentDuration.minus(Duration.days(days))
val hours = currentDuration.inWholeHours.toInt()
currentDuration = currentDuration.minus(Duration.hours(hours))
val minutes = currentDuration.inWholeMinutes.toInt()
- return if (days <= 0 && hours <= 0) {
- "${minutes}m"
+ if (days <= 0 && hours <= 0) {
+ getString(R.string.anime_next_episode_airing_minutes, minutes)
} else if (days <= 0) {
- "${hours}h ${minutes}m"
+ getString(R.string.anime_next_episode_airing_hours, hours, minutes)
} else {
- "${days}d ${hours}h ${minutes}m"
+ getString(R.string.anime_next_episode_airing_days, days, hours, minutes)
}
- } catch (e: Exception) {
- e.printStackTrace()
- return "0h 0m 0s"
+ }.getOrElse {
+ it.printStackTrace()
+ getString(R.string.anime_next_episode_airing_n_a)
}
+ return buildString {
+ append(getString(R.string.anime_next_episode_prefix))
+ append(nextEp.episode)
+ append(getString(R.string.anime_next_episode_suffix))
+ append(timeFromNow)
+ }
+}
+
+internal fun Context.showNoActionOkDialog(@StringRes title: Int, content: CharSequence?) {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(title)
+ .setMessage(
+ content.ifNullOrBlank { getString(R.string.n_a) }
+ ).setPositiveButton(R.string.ok) { dialog, _ ->
+ dialog.dismiss()
+ }.show()
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/common/extensions/StringExtensions.kt b/app/src/main/java/com/sharkaboi/mediahub/common/extensions/StringExtensions.kt
index f61a03d..20f9fc7 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/common/extensions/StringExtensions.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/common/extensions/StringExtensions.kt
@@ -9,6 +9,13 @@ internal inline var String.Companion.emptyString: String
get() = ""
private set(_) {}
+internal fun CharSequence?.ifNullOrBlank(block: () -> String): CharSequence {
+ if (this == null || this.isBlank()) {
+ return block()
+ }
+ return this
+}
+
internal fun String?.ifNullOrBlank(block: () -> String): String {
if (this == null || this.isBlank()) {
return block()
@@ -17,13 +24,10 @@ internal fun String?.ifNullOrBlank(block: () -> String): String {
}
internal fun String.tryParseDateTime(): LocalDateTime? {
- return try {
+ return runCatching {
val format = DateTimeFormatter.ISO_DATE_TIME
LocalDateTime.parse(this, format)
- } catch (e: Exception) {
- e.printStackTrace()
- null
- }
+ }.getOrNullWithStackTrace()
}
internal fun String.replaceWhiteSpaceWithUnderScore(): String {
diff --git a/app/src/main/java/com/sharkaboi/mediahub/common/extensions/UtilExtensions.kt b/app/src/main/java/com/sharkaboi/mediahub/common/extensions/UtilExtensions.kt
index ad3bae6..5c13aee 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/common/extensions/UtilExtensions.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/common/extensions/UtilExtensions.kt
@@ -2,6 +2,7 @@ package com.sharkaboi.mediahub.common.extensions
import android.content.Context
import android.widget.Toast
+import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
@@ -11,5 +12,8 @@ internal fun AppCompatActivity.showToast(message: String?, length: Int = Toast.L
internal fun Fragment.showToast(message: String?, length: Int = Toast.LENGTH_SHORT) =
Toast.makeText(context, message ?: String.emptyString, length).show()
+internal fun Fragment.showToast(@StringRes id: Int, length: Int = Toast.LENGTH_SHORT) =
+ Toast.makeText(context, id, length).show()
+
internal fun Context.showToast(message: String?, length: Int = Toast.LENGTH_SHORT) =
Toast.makeText(this, message ?: String.emptyString, length).show()
diff --git a/app/src/main/java/com/sharkaboi/mediahub/common/util/MPAndroidChartValueFormatter.kt b/app/src/main/java/com/sharkaboi/mediahub/common/util/MPAndroidChartValueFormatter.kt
index a221d80..a0c12b0 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/common/util/MPAndroidChartValueFormatter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/common/util/MPAndroidChartValueFormatter.kt
@@ -1,32 +1,33 @@
package com.sharkaboi.mediahub.common.util
import com.github.mikephil.charting.formatter.ValueFormatter
+import com.sharkaboi.mediahub.common.extensions.emptyString
import java.text.DecimalFormat
class MPAndroidChartValueFormatter : ValueFormatter() {
- private var mSuffix = arrayOf(
- "", "k", "m", "b", "t"
- )
- private var mMaxLength = 5
- private var mFormat: DecimalFormat = DecimalFormat("###E00")
+ /**
+ * Blank -> Less than 1000 (Exact)
+ * k -> 1k, 5.5k (Round off)
+ * m -> 1m, 1.8m (Round off)
+ * b -> 1b, 1.8b (Round off)
+ * t -> 1t, 1.8t (Round off)
+ */
+ private val suffixes = listOf("", "k", "m", "b", "t")
override fun getFormattedValue(value: Float): String {
- return makePretty(value.toDouble())
+ return format(value.toDouble())
}
- private fun makePretty(number: Double): String {
+ private fun format(number: Double): String {
if (number == 0.0) {
- return ""
+ return String.emptyString
}
- var r = mFormat.format(number)
- val numericValue1 = Character.getNumericValue(r[r.length - 1])
- val numericValue2 = Character.getNumericValue(r[r.length - 2])
- val combined = Integer.valueOf(numericValue2.toString() + "" + numericValue1)
- r = r.replace("E[0-9][0-9]".toRegex(), mSuffix[combined / 3])
- while (r.length > mMaxLength || r.matches(Regex("[0-9]+\\.[a-z]"))) {
- r = r.substring(0, r.length - 2) + r.substring(r.length - 1)
- }
- return r
+ val exponentFormatter = DecimalFormat("###E00")
+ val formattedValue = exponentFormatter.format(number)
+ val base = formattedValue.dropLast(3).toLong()
+ val exponent = formattedValue.takeLast(2).toInt()
+ val currentSuffix = suffixes[exponent / 3]
+ return "$base$currentSuffix"
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/common/util/UrlLauncher.kt b/app/src/main/java/com/sharkaboi/mediahub/common/util/UrlLauncher.kt
index 11cdbc3..ebd3bd1 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/common/util/UrlLauncher.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/common/util/UrlLauncher.kt
@@ -3,17 +3,20 @@ package com.sharkaboi.mediahub.common.util
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
-import android.net.Uri
+import android.content.pm.PackageManager
+import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import com.sharkaboi.mediahub.R
+import com.sharkaboi.mediahub.common.constants.AppConstants
import com.sharkaboi.mediahub.common.extensions.showToast
+import timber.log.Timber
fun Activity.openUrl(url: String) {
try {
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
+ startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
} catch (e: ActivityNotFoundException) {
- showToast("No browser found in the phone")
+ showToast(getString(R.string.no_browser_found_hint))
} catch (e: Exception) {
showToast(e.message)
}
@@ -21,45 +24,61 @@ fun Activity.openUrl(url: String) {
fun Fragment.openUrl(url: String) {
try {
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
+ startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
activity?.overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
} catch (e: ActivityNotFoundException) {
- showToast("No browser found in the phone")
+ showToast(getString(R.string.no_browser_found_hint))
} catch (e: Exception) {
showToast(e.message)
}
}
-fun Activity.openShareChooser(content: String, shareHint: String = "Share here") {
+fun Fragment.openShareChooser(content: String, shareHint: String = getString(R.string.share_hint)) {
try {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, content)
- type = "text/plain"
+ setTypeAndNormalize("text/plain")
}
val shareIntent = Intent.createChooser(sendIntent, shareHint)
startActivity(shareIntent)
- overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
+ activity?.overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
} catch (e: ActivityNotFoundException) {
- showToast("No browser found in the phone")
+ showToast(getString(R.string.no_browser_found_hint))
} catch (e: Exception) {
showToast(e.message)
}
}
-fun Fragment.openShareChooser(content: String, shareHint: String = "Share here") {
+/**
+ * Uses package manager to get all activities that can open a non-deeplinked https uri.
+ * Then forces the url specified to be opened by the first activity among the possible activities.
+ * Excludes Mediahub's package name and sorts by default to get most optimal browser.
+ */
+fun Activity.forceLaunchInBrowser(url: String) {
try {
- val sendIntent: Intent = Intent().apply {
- action = Intent.ACTION_SEND
- putExtra(Intent.EXTRA_TEXT, content)
- type = "text/plain"
+ val browserIntent = Intent(
+ Intent.ACTION_VIEW,
+ AppConstants.nonDeepLinkedUrl.toUri()
+ )
+ val browsersList = packageManager.queryIntentActivities(
+ browserIntent, PackageManager.MATCH_DEFAULT_ONLY
+ )
+ Timber.d(browsersList.toString())
+ val filteredList = browsersList.filter {
+ it.activityInfo.packageName != this.packageName
}
- val shareIntent = Intent.createChooser(sendIntent, shareHint)
- startActivity(shareIntent)
- activity?.overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
+ val targetedBrowserIntent = Intent(Intent.ACTION_VIEW, url.toUri()).apply {
+ setPackage(filteredList.first().activityInfo.packageName)
+ }
+ startActivity(targetedBrowserIntent)
+ overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
+ } catch (e: NoSuchElementException) {
+ showToast(getString(R.string.no_browser_found_hint))
} catch (e: ActivityNotFoundException) {
- showToast("No browser found in the phone")
+ showToast(getString(R.string.no_browser_found_hint))
} catch (e: Exception) {
+ e.printStackTrace()
showToast(e.message)
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/common/views/CustomProgressScreen.kt b/app/src/main/java/com/sharkaboi/mediahub/common/views/CustomProgressScreen.kt
index 71cb411..527e8da 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/common/views/CustomProgressScreen.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/common/views/CustomProgressScreen.kt
@@ -10,7 +10,7 @@ import androidx.core.view.isVisible
import com.sharkaboi.mediahub.R
/**
- * Progress view with dimmed bg
+ * Progress view with dimmed background
* that shows only if hide isn't called within set time
* and hides only if set delay is over
*/
diff --git a/app/src/main/java/com/sharkaboi/mediahub/common/views/image_slider/FullScreenImageFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/common/views/image_slider/FullScreenImageFragment.kt
index 519207d..f4b44af 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/common/views/image_slider/FullScreenImageFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/common/views/image_slider/FullScreenImageFragment.kt
@@ -7,6 +7,7 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import coil.load
import com.sharkaboi.mediahub.R
+import com.sharkaboi.mediahub.common.extensions.emptyString
import com.sharkaboi.mediahub.databinding.FragmentFullScreenImageBinding
class FullScreenImageFragment : Fragment() {
@@ -17,7 +18,7 @@ class FullScreenImageFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
- imageUrl = it.getString(IMAGE_URL_KEY) ?: ""
+ imageUrl = it.getString(IMAGE_URL_KEY) ?: String.emptyString
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/ApiConstants.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/constants/ApiConstants.kt
similarity index 89%
rename from app/src/main/java/com/sharkaboi/mediahub/data/api/ApiConstants.kt
rename to app/src/main/java/com/sharkaboi/mediahub/data/api/constants/ApiConstants.kt
index 5e0097a..14950f2 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/api/ApiConstants.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/constants/ApiConstants.kt
@@ -1,4 +1,4 @@
-package com.sharkaboi.mediahub.data.api
+package com.sharkaboi.mediahub.data.api.constants
object ApiConstants {
const val BEARER_SEPARATOR = "Bearer "
@@ -16,4 +16,5 @@ object ApiConstants {
const val MANGA_ALL_FIELDS =
"id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,created_at,updated_at,media_type,status,genres,my_list_status,num_volumes,num_chapters,authors{first_name,last_name},pictures,background,related_anime,related_manga,recommendations,serialization{name}"
const val PROFILE_FIELDS = "anime_statistics"
+ val appAcceptedDeepLinkRegex = """^(https://myanimelist.net/(anime|manga)/(\d+)/([^/])+)$""".toRegex()
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/constants/MALExternalLinks.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/constants/MALExternalLinks.kt
new file mode 100644
index 0000000..9fd641e
--- /dev/null
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/constants/MALExternalLinks.kt
@@ -0,0 +1,115 @@
+package com.sharkaboi.mediahub.data.api.constants
+
+import com.sharkaboi.mediahub.common.extensions.replaceWhiteSpaceWithUnderScore
+import com.sharkaboi.mediahub.data.api.models.anime.AnimeByIDResponse
+import com.sharkaboi.mediahub.data.api.models.manga.MangaByIDResponse
+
+object MALExternalLinks {
+ fun getAnimeGenresLink(genre: AnimeByIDResponse.Genre): String {
+ return "https://myanimelist.net/anime/genre" +
+ "/${genre.id}/${genre.name.replaceWhiteSpaceWithUnderScore()}"
+ }
+
+ fun getAnimeCharactersLink(id: Int, title: String): String {
+ return "https://myanimelist.net/anime" +
+ "/$id/${title.replaceWhiteSpaceWithUnderScore()}/characters"
+ }
+
+ fun getAnimeStaffLink(id: Int, title: String): String {
+ return "https://myanimelist.net/anime" +
+ "/$id/${title.replaceWhiteSpaceWithUnderScore()}/characters#staff"
+ }
+
+ fun getAnimeReviewsLink(id: Int, title: String): String {
+ return "https://myanimelist.net/anime" +
+ "/$id/${title.replaceWhiteSpaceWithUnderScore()}/reviews"
+ }
+
+ fun getAnimeNewsLink(id: Int, title: String): String {
+ return "https://myanimelist.net/anime" +
+ "/$id/${title.replaceWhiteSpaceWithUnderScore()}/news"
+ }
+
+ fun getAnimeVideosLink(id: Int, title: String): String {
+ return "https://myanimelist.net/anime" +
+ "/$id/${title.replaceWhiteSpaceWithUnderScore()}/video"
+ }
+
+ fun getAnimeProducerPageLink(studio: AnimeByIDResponse.Studio): String {
+ return "https://myanimelist.net/anime/producer" +
+ "/${studio.id}/${studio.name.replaceWhiteSpaceWithUnderScore()}"
+ }
+
+ fun getMangaAuthorPageLink(author: MangaByIDResponse.Author): String {
+ val name =
+ "${author.node.firstName} ${author.node.lastName}"
+ .replaceWhiteSpaceWithUnderScore()
+ return "https://myanimelist.net/people" +
+ "/${author.node.id}/$name"
+ }
+
+ fun getMangaGenresLink(genre: MangaByIDResponse.Genre): String {
+ return "https://myanimelist.net/manga/genre" +
+ "/${genre.id}/${genre.name.replaceWhiteSpaceWithUnderScore()}"
+ }
+
+ fun getMangaCharactersLink(id: Int, title: String): String {
+ return "https://myanimelist.net/manga" +
+ "/$id/${title.replaceWhiteSpaceWithUnderScore()}/characters"
+ }
+
+ fun getMangaReviewsLink(id: Int, title: String): String {
+ return "https://myanimelist.net/manga" +
+ "/$id/${title.replaceWhiteSpaceWithUnderScore()}/reviews"
+ }
+
+ fun getMangaNewsLink(id: Int, title: String): String {
+ return "https://myanimelist.net/manga" +
+ "/$id/${title.replaceWhiteSpaceWithUnderScore()}/news"
+ }
+
+ fun getMangaSerializationPageLink(magazine: MangaByIDResponse.Serialization): String {
+ return "https://myanimelist.net/manga/magazine" +
+ "/${magazine.node.id}/${magazine.node.name.replaceWhiteSpaceWithUnderScore()}"
+ }
+
+ fun getBlogsLink(name: String): String {
+ return "https://myanimelist.net/blog/$name"
+ }
+
+ fun getClubsLink(name: String): String {
+ return "https://myanimelist.net/profile/$name/clubs"
+ }
+
+ fun getForumTopicsLink(name: String): String {
+ return "https://myanimelist.net/forum/search?u=$name&q=&uloc=1&loc=-1"
+ }
+
+ fun getFriendsLink(name: String): String {
+ return "https://myanimelist.net/profile/$name/friends"
+ }
+
+ fun getHistoryLink(name: String): String {
+ return "https://myanimelist.net/history/$name"
+ }
+
+ fun getRecommendationsLink(name: String): String {
+ return "https://myanimelist.net/profile/$name/recommendations"
+ }
+
+ fun getReviewsLink(name: String): String {
+ return "https://myanimelist.net/profile/$name/reviews"
+ }
+
+ fun getProfileLink(name: String): String {
+ return "https://myanimelist.net/profile/$name"
+ }
+
+ fun getUserAnimeListLink(name: String): String {
+ return "https://myanimelist.net/animelist/$name"
+ }
+
+ fun getUserMangaListLink(name: String): String {
+ return "https://myanimelist.net/mangalist/$name"
+ }
+}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeAiringStatus.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeAiringStatus.kt
new file mode 100644
index 0000000..27ab6ce
--- /dev/null
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeAiringStatus.kt
@@ -0,0 +1,29 @@
+@file:Suppress("EnumEntryName")
+
+package com.sharkaboi.mediahub.data.api.enums
+
+import android.content.Context
+import com.sharkaboi.mediahub.R
+
+enum class AnimeAiringStatus {
+ finished_airing,
+ currently_airing,
+ not_yet_aired
+}
+
+internal fun Context.getAnimeAiringStatus(status: String): String {
+ return when {
+ status.trim() == AnimeAiringStatus.finished_airing.name -> {
+ getString(R.string.anime_airing_finished)
+ }
+ status.trim() == AnimeAiringStatus.currently_airing.name -> {
+ getString(R.string.anime_airing_ongoing)
+ }
+ status.trim() == AnimeAiringStatus.not_yet_aired.name -> {
+ getString(R.string.anime_airing_not_yet_aired)
+ }
+ else -> {
+ getString(R.string.anime_airing_unknown)
+ }
+ }
+}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeNsfwRating.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeNsfwRating.kt
new file mode 100644
index 0000000..acadfd2
--- /dev/null
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeNsfwRating.kt
@@ -0,0 +1,30 @@
+package com.sharkaboi.mediahub.data.api.enums
+
+import android.content.Context
+import com.sharkaboi.mediahub.R
+
+enum class AnimeNsfwRating {
+ white,
+ gray,
+ black
+}
+
+internal fun Context.getAnimeNsfwRating(rating: String?): String {
+ return when {
+ rating == null -> {
+ getString(R.string.n_a)
+ }
+ rating.trim() == AnimeNsfwRating.white.name -> {
+ getString(R.string.anime_nsfw_rating_white)
+ }
+ rating.trim() == AnimeNsfwRating.gray.name -> {
+ getString(R.string.anime_nsfw_rating_gray)
+ }
+ rating.trim() == AnimeNsfwRating.black.name -> {
+ getString(R.string.anime_nsfw_rating_black)
+ }
+ else -> {
+ getString(R.string.anime_nsfw_rating_unknown)
+ }
+ }
+}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeRankingType.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeRankingType.kt
index f33c9bc..ea02b15 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeRankingType.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeRankingType.kt
@@ -1,5 +1,8 @@
package com.sharkaboi.mediahub.data.api.enums
+import android.content.Context
+import com.sharkaboi.mediahub.R
+
@Suppress("EnumEntryName")
enum class AnimeRankingType {
all, // Top Anime Series
@@ -12,17 +15,17 @@ enum class AnimeRankingType {
bypopularity, // Top Anime by Popularity
favorite; // Top Favorited Anime
- fun getAnimeRanking(): String {
+ fun getAnimeRanking(context: Context): String {
return when (this) {
- all -> "All"
- airing -> "Airing"
- upcoming -> "Upcoming"
- tv -> "TV"
- ova -> "OVA"
- movie -> "Movie"
- special -> "Specials"
- bypopularity -> "By popularity"
- favorite -> "In your list"
+ all -> context.getString(R.string.anime_ranking_all)
+ airing -> context.getString(R.string.anime_ranking_airing)
+ upcoming -> context.getString(R.string.anime_ranking_upcoming)
+ tv -> context.getString(R.string.anime_ranking_tv)
+ ova -> context.getString(R.string.anime_ranking_ova)
+ movie -> context.getString(R.string.anime_ranking_movie)
+ special -> context.getString(R.string.anime_ranking_specials)
+ bypopularity -> context.getString(R.string.anime_ranking_by_popularity)
+ favorite -> context.getString(R.string.anime_ranking_in_your_list)
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeRating.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeRating.kt
new file mode 100644
index 0000000..d1d7eef
--- /dev/null
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeRating.kt
@@ -0,0 +1,26 @@
+package com.sharkaboi.mediahub.data.api.enums
+
+import android.content.Context
+import com.sharkaboi.mediahub.R
+
+object AnimeRating {
+ private const val G = "g"
+ private const val PG = "pg"
+ private const val PG13 = "pg_13"
+ private const val rRating = "r"
+ private const val rPlusRating = "r+"
+ private const val RX = "rx"
+
+ fun Context.getAnimeRating(rating: String?): String {
+ return when {
+ rating == null -> getString(R.string.n_a)
+ rating.trim() == G -> getString(R.string.anime_rating_g)
+ rating.trim() == PG -> getString(R.string.anime_rating_pg)
+ rating.trim() == PG13 -> getString(R.string.anime_rating_pg13)
+ rating.trim() == rRating -> getString(R.string.anime_rating_r)
+ rating.trim() == rPlusRating -> getString(R.string.anime_rating_r_plus)
+ rating.trim() == RX -> getString(R.string.anime_rating_rx)
+ else -> getString(R.string.n_a)
+ }
+ }
+}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeStatus.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeStatus.kt
index fe5feb5..4f667d0 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeStatus.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/AnimeStatus.kt
@@ -1,5 +1,8 @@
package com.sharkaboi.mediahub.data.api.enums
+import android.content.Context
+import com.sharkaboi.mediahub.R
+
@Suppress("EnumEntryName")
enum class AnimeStatus {
watching,
@@ -9,14 +12,14 @@ enum class AnimeStatus {
dropped,
all;
- fun getFormattedString(): String {
+ fun getFormattedString(context: Context): String {
return when (this) {
- watching -> "Watching"
- plan_to_watch -> "Planned"
- completed -> "Completed"
- on_hold -> "On hold"
- dropped -> "Dropped"
- all -> "All"
+ watching -> context.getString(R.string.anime_status_watching)
+ plan_to_watch -> context.getString(R.string.anime_status_planned)
+ completed -> context.getString(R.string.anime_status_completed)
+ on_hold -> context.getString(R.string.anime_status_on_hold)
+ dropped -> context.getString(R.string.anime_status_dropped)
+ all -> context.getString(R.string.anime_status_all)
}
}
@@ -27,11 +30,11 @@ enum class AnimeStatus {
fun String.animeStatusFromString(): AnimeStatus? {
return when (this) {
- "watching" -> AnimeStatus.watching
- "plan_to_watch" -> AnimeStatus.plan_to_watch
- "completed" -> AnimeStatus.completed
- "on_hold" -> AnimeStatus.on_hold
- "dropped" -> AnimeStatus.dropped
+ AnimeStatus.watching.name -> AnimeStatus.watching
+ AnimeStatus.plan_to_watch.name -> AnimeStatus.plan_to_watch
+ AnimeStatus.completed.name -> AnimeStatus.completed
+ AnimeStatus.on_hold.name -> AnimeStatus.on_hold
+ AnimeStatus.dropped.name -> AnimeStatus.dropped
else -> null // AnimeStatus.all
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaNsfwRating.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaNsfwRating.kt
new file mode 100644
index 0000000..098cb7c
--- /dev/null
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaNsfwRating.kt
@@ -0,0 +1,20 @@
+package com.sharkaboi.mediahub.data.api.enums
+
+import android.content.Context
+import com.sharkaboi.mediahub.R
+
+enum class MangaNsfwRating {
+ white,
+ gray,
+ black
+}
+
+internal fun Context.getMangaNsfwRating(rating: String?): String {
+ return when {
+ rating == null -> getString(R.string.n_a)
+ rating.trim() == MangaNsfwRating.white.name -> getString(R.string.manga_nsfw_rating_white)
+ rating.trim() == MangaNsfwRating.gray.name -> getString(R.string.manga_nsfw_rating_gray)
+ rating.trim() == MangaNsfwRating.black.name -> getString(R.string.manga_nsfw_rating_black)
+ else -> getString(R.string.n_a)
+ }
+}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaPublishingStatus.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaPublishingStatus.kt
new file mode 100644
index 0000000..bbe4736
--- /dev/null
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaPublishingStatus.kt
@@ -0,0 +1,19 @@
+package com.sharkaboi.mediahub.data.api.enums
+
+import android.content.Context
+import com.sharkaboi.mediahub.R
+
+enum class MangaPublishingStatus {
+ finished,
+ currently_publishing,
+ not_yet_published
+}
+
+internal fun Context.getMangaPublishStatus(status: String): String {
+ return when {
+ status.trim() == MangaPublishingStatus.finished.name -> getString(R.string.manga_publishing_finished)
+ status.trim() == MangaPublishingStatus.currently_publishing.name -> getString(R.string.manga_publishing_ongoing)
+ status.trim() == MangaPublishingStatus.not_yet_published.name -> getString(R.string.manga_publishing_not_yet_aired)
+ else -> getString(R.string.manga_publishing_unknown)
+ }
+}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaRankingType.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaRankingType.kt
index 5e9addf..9191cb7 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaRankingType.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaRankingType.kt
@@ -1,5 +1,8 @@
package com.sharkaboi.mediahub.data.api.enums
+import android.content.Context
+import com.sharkaboi.mediahub.R
+
@Suppress("EnumEntryName")
enum class MangaRankingType {
all, // All
@@ -13,18 +16,33 @@ enum class MangaRankingType {
bypopularity, // Most Popular
favorite; // Most Favorited
- fun getFormattedString(): String {
+ fun getFormattedString(context: Context): String {
return when (this) {
- all -> "All"
- manga -> "Manga"
- oneshots -> "One-shots"
- doujin -> "Doujins"
- lightnovels -> "Light novels"
- novels -> "Novels"
- manhwa -> "Manhwa"
- manhua -> "Manhua"
- bypopularity -> "By popularity"
- favorite -> "In your list"
+ all -> context.getString(R.string.manga_ranking_all)
+ manga -> context.getString(R.string.manga_ranking_manga)
+ oneshots -> context.getString(R.string.manga_ranking_one_shot)
+ doujin -> context.getString(R.string.manga_ranking_doujins)
+ lightnovels -> context.getString(R.string.manga_ranking_light_novels)
+ novels -> context.getString(R.string.manga_ranking_novels)
+ manhwa -> context.getString(R.string.manga_ranking_manhwa)
+ manhua -> context.getString(R.string.manga_ranking_manhua)
+ bypopularity -> context.getString(R.string.manga_ranking_by_popularity)
+ favorite -> context.getString(R.string.manga_ranking_in_your_list)
+ }
+ }
+
+ companion object {
+ private const val lightNovelSlug = "light_novel"
+ private const val oneShotSlug = "one_shot"
+ private const val doujinshiSlug = "doujinshi"
+ fun getMangaRankingFromString(ranking: String?): MangaRankingType {
+ return when {
+ ranking == null -> all
+ ranking.lowercase() == lightNovelSlug -> lightnovels
+ ranking.lowercase() == oneShotSlug -> oneshots
+ ranking.lowercase() == doujinshiSlug -> doujin
+ else -> runCatching { valueOf(ranking.lowercase()) }.getOrElse { all }
+ }
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaStatus.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaStatus.kt
index ea873cf..dfdc990 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaStatus.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/MangaStatus.kt
@@ -1,5 +1,8 @@
package com.sharkaboi.mediahub.data.api.enums
+import android.content.Context
+import com.sharkaboi.mediahub.R
+
@Suppress("EnumEntryName")
enum class MangaStatus {
reading,
@@ -9,14 +12,14 @@ enum class MangaStatus {
dropped,
all;
- fun getFormattedString(): String {
+ fun getFormattedString(context: Context): String {
return when (this) {
- reading -> "Reading"
- plan_to_read -> "Planned"
- completed -> "Completed"
- on_hold -> "On hold"
- dropped -> "Dropped"
- all -> "All"
+ reading -> context.getString(R.string.manga_status_reading)
+ plan_to_read -> context.getString(R.string.manga_status_planned)
+ completed -> context.getString(R.string.manga_status_completed)
+ on_hold -> context.getString(R.string.manga_status_on_hold)
+ dropped -> context.getString(R.string.manga_status_dropped)
+ all -> context.getString(R.string.manga_status_all)
}
}
@@ -27,11 +30,11 @@ enum class MangaStatus {
fun String.mangaStatusFromString(): MangaStatus? {
return when (this) {
- "reading" -> MangaStatus.reading
- "plan_to_read" -> MangaStatus.plan_to_read
- "completed" -> MangaStatus.completed
- "on_hold" -> MangaStatus.on_hold
- "dropped" -> MangaStatus.dropped
+ MangaStatus.reading.name -> MangaStatus.reading
+ MangaStatus.plan_to_read.name -> MangaStatus.plan_to_read
+ MangaStatus.completed.name -> MangaStatus.completed
+ MangaStatus.on_hold.name -> MangaStatus.on_hold
+ MangaStatus.dropped.name -> MangaStatus.dropped
else -> null // MangaStatus.all
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/UserAnimeSortType.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/UserAnimeSortType.kt
index cec7276..7f7a51e 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/UserAnimeSortType.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/UserAnimeSortType.kt
@@ -1,5 +1,8 @@
package com.sharkaboi.mediahub.data.api.enums
+import android.content.Context
+import com.sharkaboi.mediahub.R
+
@Suppress("EnumEntryName")
enum class UserAnimeSortType {
list_score, // Descending
@@ -8,11 +11,11 @@ enum class UserAnimeSortType {
anime_start_date; // Descending
companion object {
- fun getFormattedArray() = arrayOf(
- "Highest rating",
- "Last updated",
- "Alphabetical order",
- "Newest addition"
+ fun getFormattedArray(context: Context) = arrayOf(
+ context.getString(R.string.user_anime_sort_by_rating),
+ context.getString(R.string.user_anime_sort_by_updated),
+ context.getString(R.string.user_anime_sort_by_alphabetical),
+ context.getString(R.string.user_anime_sort_by_newest)
)
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/UserMangaSortType.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/UserMangaSortType.kt
index 6fa5d35..d1f03f1 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/UserMangaSortType.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/enums/UserMangaSortType.kt
@@ -1,5 +1,8 @@
package com.sharkaboi.mediahub.data.api.enums
+import android.content.Context
+import com.sharkaboi.mediahub.R
+
@Suppress("EnumEntryName")
enum class UserMangaSortType {
list_score, // Descending
@@ -8,11 +11,11 @@ enum class UserMangaSortType {
manga_start_date; // Descending
companion object {
- fun getFormattedArray() = arrayOf(
- "Highest rating",
- "Last updated",
- "Alphabetical order",
- "Newest addition"
+ fun getFormattedArray(context: Context) = arrayOf(
+ context.getString(R.string.user_manga_sort_by_rating),
+ context.getString(R.string.user_manga_sort_by_updated),
+ context.getString(R.string.user_manga_sort_by_alphabetical),
+ context.getString(R.string.user_manga_sort_by_newest)
)
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/AnimeService.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/AnimeService.kt
index ef4d84f..d0b76a7 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/AnimeService.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/AnimeService.kt
@@ -1,7 +1,7 @@
package com.sharkaboi.mediahub.data.api.retrofit
import com.haroldadmin.cnradapter.NetworkResponse
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.AnimeRankingType
import com.sharkaboi.mediahub.data.api.enums.AnimeSortType
import com.sharkaboi.mediahub.data.api.models.ApiError
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/MangaService.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/MangaService.kt
index 2fd9a2d..c5ec2aa 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/MangaService.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/MangaService.kt
@@ -1,7 +1,7 @@
package com.sharkaboi.mediahub.data.api.retrofit
import com.haroldadmin.cnradapter.NetworkResponse
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.MangaRankingType
import com.sharkaboi.mediahub.data.api.models.ApiError
import com.sharkaboi.mediahub.data.api.models.manga.MangaByIDResponse
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/UserAnimeService.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/UserAnimeService.kt
index bc5f5ad..85d2a31 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/UserAnimeService.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/UserAnimeService.kt
@@ -1,7 +1,7 @@
package com.sharkaboi.mediahub.data.api.retrofit
import com.haroldadmin.cnradapter.NetworkResponse
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.UserAnimeSortType
import com.sharkaboi.mediahub.data.api.models.ApiError
import com.sharkaboi.mediahub.data.api.models.useranime.UserAnimeListResponse
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/UserMangaService.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/UserMangaService.kt
index 26ee684..95c9979 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/UserMangaService.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/UserMangaService.kt
@@ -1,7 +1,7 @@
package com.sharkaboi.mediahub.data.api.retrofit
import com.haroldadmin.cnradapter.NetworkResponse
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.UserMangaSortType
import com.sharkaboi.mediahub.data.api.models.ApiError
import com.sharkaboi.mediahub.data.api.models.usermanga.UserMangaListResponse
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/UserService.kt b/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/UserService.kt
index cd207bd..b3c2aba 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/UserService.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/api/retrofit/UserService.kt
@@ -1,7 +1,7 @@
package com.sharkaboi.mediahub.data.api.retrofit
import com.haroldadmin.cnradapter.NetworkResponse
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.models.ApiError
import com.sharkaboi.mediahub.data.api.models.user.UserDetailsResponse
import kotlinx.coroutines.Deferred
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/datastore/DataStoreRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/data/datastore/DataStoreRepositoryImpl.kt
index 11a2740..5124ce5 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/datastore/DataStoreRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/datastore/DataStoreRepositoryImpl.kt
@@ -11,10 +11,13 @@ import com.sharkaboi.mediahub.data.datastore.DataStoreConstants.REFRESH_TOKEN
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
+import timber.log.Timber
import java.io.IOException
import java.util.*
+import kotlin.coroutines.coroutineContext
internal val Context.dataStore by preferencesDataStore(
name = DataStoreConstants.PREFERENCES_NAME
@@ -30,8 +33,9 @@ class DataStoreRepositoryImpl(
throw exception
}
}.map { preferences ->
+ Timber.d("Collected access token on $coroutineContext")
preferences[ACCESS_TOKEN]
- }
+ }.flowOn(Dispatchers.IO)
override val refreshTokenFlow: Flow = dataStore.data.catch { exception ->
if (exception is IOException) {
@@ -45,7 +49,7 @@ class DataStoreRepositoryImpl(
} ?: run {
return@map String.emptyString
}
- }
+ }.flowOn(Dispatchers.IO)
override val expiresInFlow: Flow = dataStore.data.catch { exception ->
if (exception is IOException) {
@@ -59,7 +63,7 @@ class DataStoreRepositoryImpl(
timeInMillis = expiredIn
}
date.time
- }
+ }.flowOn(Dispatchers.IO)
override suspend fun setAccessToken(token: String): Unit = withContext(Dispatchers.IO) {
dataStore.edit { preferences ->
@@ -82,7 +86,7 @@ class DataStoreRepositoryImpl(
}
}
- override suspend fun clearDataStore() {
+ override suspend fun clearDataStore(): Unit = withContext(Dispatchers.IO) {
dataStore.edit { preferences ->
preferences.clear()
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeRankingDataSource.kt b/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeRankingDataSource.kt
index 7689364..c4efe16 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeRankingDataSource.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeRankingDataSource.kt
@@ -4,12 +4,14 @@ import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.haroldadmin.cnradapter.NetworkResponse
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.AnimeRankingType
import com.sharkaboi.mediahub.data.api.models.ApiError
import com.sharkaboi.mediahub.data.api.models.anime.AnimeRankingResponse
import com.sharkaboi.mediahub.data.api.retrofit.AnimeService
-import com.sharkaboi.mediahub.data.wrappers.NoTokenFoundError
+import com.sharkaboi.mediahub.data.wrappers.MHError
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import timber.log.Timber
class AnimeRankingDataSource(
@@ -19,14 +21,12 @@ class AnimeRankingDataSource(
private val showNsfw: Boolean = false
) : PagingSource() {
+ /**
+ * prevKey == null -> first page
+ * nextKey == null -> last page
+ * both prevKey and nextKey null -> only one page
+ */
override fun getRefreshKey(state: PagingState): Int? {
- // Try to find the page key of the closest page to anchorPosition, from
- // either the prevKey or the nextKey, but you need to handle nullability
- // here:
- // * prevKey == null -> anchorPage is the first page.
- // * nextKey == null -> anchorPage is the last page.
- // * both prevKey and nextKey null -> anchorPage is the initial page, so
- // just return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(ApiConstants.API_PAGE_LIMIT) ?: anchorPage?.nextKey?.minus(
@@ -42,16 +42,18 @@ class AnimeRankingDataSource(
val limit = ApiConstants.API_PAGE_LIMIT
if (accessToken == null) {
return LoadResult.Error(
- NoTokenFoundError.getThrowable()
+ MHError.LoginExpiredError.getThrowable()
)
} else {
- val response = animeService.getAnimeRankingAsync(
- authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
- offset = offset,
- limit = limit,
- rankingType = animeRankingType.name,
- nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
- ).await()
+ val response = withContext(Dispatchers.IO) {
+ animeService.getAnimeRankingAsync(
+ authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
+ offset = offset,
+ limit = limit,
+ rankingType = animeRankingType.name,
+ nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
+ ).await()
+ }
when (response) {
is NetworkResponse.Success -> {
val nextOffset = if (response.body.data.isEmpty()) null else offset + limit
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeSearchDataSource.kt b/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeSearchDataSource.kt
index 3bad4b9..2977caf 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeSearchDataSource.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeSearchDataSource.kt
@@ -4,11 +4,13 @@ import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.haroldadmin.cnradapter.NetworkResponse
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.models.ApiError
import com.sharkaboi.mediahub.data.api.models.anime.AnimeSearchResponse
import com.sharkaboi.mediahub.data.api.retrofit.AnimeService
-import com.sharkaboi.mediahub.data.wrappers.NoTokenFoundError
+import com.sharkaboi.mediahub.data.wrappers.MHError
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import timber.log.Timber
class AnimeSearchDataSource(
@@ -18,14 +20,12 @@ class AnimeSearchDataSource(
private val showNsfw: Boolean = false
) : PagingSource() {
+ /**
+ * prevKey == null -> first page
+ * nextKey == null -> last page
+ * both prevKey and nextKey null -> only one page
+ */
override fun getRefreshKey(state: PagingState): Int? {
- // Try to find the page key of the closest page to anchorPosition, from
- // either the prevKey or the nextKey, but you need to handle nullability
- // here:
- // * prevKey == null -> anchorPage is the first page.
- // * nextKey == null -> anchorPage is the last page.
- // * both prevKey and nextKey null -> anchorPage is the initial page, so
- // just return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(ApiConstants.API_PAGE_LIMIT) ?: anchorPage?.nextKey?.minus(
@@ -41,16 +41,18 @@ class AnimeSearchDataSource(
val limit = ApiConstants.API_PAGE_LIMIT
if (accessToken == null) {
return LoadResult.Error(
- NoTokenFoundError.getThrowable()
+ MHError.LoginExpiredError.getThrowable()
)
} else {
- val response = animeService.getAnimeAsync(
- authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
- offset = offset,
- limit = limit,
- searchQuery = query,
- nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
- ).await()
+ val response = withContext(Dispatchers.IO) {
+ animeService.getAnimeAsync(
+ authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
+ offset = offset,
+ limit = limit,
+ searchQuery = query,
+ nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
+ ).await()
+ }
when (response) {
is NetworkResponse.Success -> {
val nextOffset = if (response.body.data.isEmpty()) null else offset + limit
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeSeasonalDataSource.kt b/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeSeasonalDataSource.kt
index 58c222a..2ce278f 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeSeasonalDataSource.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeSeasonalDataSource.kt
@@ -4,12 +4,14 @@ import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.haroldadmin.cnradapter.NetworkResponse
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.AnimeSeason
import com.sharkaboi.mediahub.data.api.models.ApiError
import com.sharkaboi.mediahub.data.api.models.anime.AnimeSeasonalResponse
import com.sharkaboi.mediahub.data.api.retrofit.AnimeService
-import com.sharkaboi.mediahub.data.wrappers.NoTokenFoundError
+import com.sharkaboi.mediahub.data.wrappers.MHError
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import timber.log.Timber
class AnimeSeasonalDataSource(
@@ -20,14 +22,12 @@ class AnimeSeasonalDataSource(
private val showNsfw: Boolean = false
) : PagingSource() {
+ /**
+ * prevKey == null -> first page
+ * nextKey == null -> last page
+ * both prevKey and nextKey null -> only one page
+ */
override fun getRefreshKey(state: PagingState): Int? {
- // Try to find the page key of the closest page to anchorPosition, from
- // either the prevKey or the nextKey, but you need to handle nullability
- // here:
- // * prevKey == null -> anchorPage is the first page.
- // * nextKey == null -> anchorPage is the last page.
- // * both prevKey and nextKey null -> anchorPage is the initial page, so
- // just return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(ApiConstants.API_PAGE_LIMIT) ?: anchorPage?.nextKey?.minus(
@@ -43,17 +43,19 @@ class AnimeSeasonalDataSource(
val limit = ApiConstants.API_PAGE_LIMIT
if (accessToken == null) {
return LoadResult.Error(
- NoTokenFoundError.getThrowable()
+ MHError.LoginExpiredError.getThrowable()
)
} else {
- val response = animeService.getAnimeBySeasonAsync(
- authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
- offset = offset,
- limit = limit,
- season = animeSeason.name,
- year = year,
- nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
- ).await()
+ val response = withContext(Dispatchers.IO) {
+ animeService.getAnimeBySeasonAsync(
+ authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
+ offset = offset,
+ limit = limit,
+ season = animeSeason.name,
+ year = year,
+ nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
+ ).await()
+ }
when (response) {
is NetworkResponse.Success -> {
val nextOffset = if (response.body.data.isEmpty()) null else offset + limit
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeSuggestionsDataSource.kt b/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeSuggestionsDataSource.kt
index a1710cc..ecdb4b5 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeSuggestionsDataSource.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/paging/AnimeSuggestionsDataSource.kt
@@ -4,11 +4,13 @@ import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.haroldadmin.cnradapter.NetworkResponse
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.models.ApiError
import com.sharkaboi.mediahub.data.api.models.anime.AnimeSuggestionsResponse
import com.sharkaboi.mediahub.data.api.retrofit.AnimeService
-import com.sharkaboi.mediahub.data.wrappers.NoTokenFoundError
+import com.sharkaboi.mediahub.data.wrappers.MHError
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import timber.log.Timber
class AnimeSuggestionsDataSource(
@@ -17,14 +19,12 @@ class AnimeSuggestionsDataSource(
private val showNsfw: Boolean = false
) : PagingSource() {
+ /**
+ * prevKey == null -> first page
+ * nextKey == null -> last page
+ * both prevKey and nextKey null -> only one page
+ */
override fun getRefreshKey(state: PagingState): Int? {
- // Try to find the page key of the closest page to anchorPosition, from
- // either the prevKey or the nextKey, but you need to handle nullability
- // here:
- // * prevKey == null -> anchorPage is the first page.
- // * nextKey == null -> anchorPage is the last page.
- // * both prevKey and nextKey null -> anchorPage is the initial page, so
- // just return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(ApiConstants.API_PAGE_LIMIT) ?: anchorPage?.nextKey?.minus(
@@ -40,15 +40,17 @@ class AnimeSuggestionsDataSource(
val limit = ApiConstants.API_PAGE_LIMIT
if (accessToken == null) {
return LoadResult.Error(
- NoTokenFoundError.getThrowable()
+ MHError.LoginExpiredError.getThrowable()
)
} else {
- val response = animeService.getAnimeSuggestionsAsync(
- authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
- offset = offset,
- limit = limit,
- nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
- ).await()
+ val response = withContext(Dispatchers.IO) {
+ animeService.getAnimeSuggestionsAsync(
+ authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
+ offset = offset,
+ limit = limit,
+ nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
+ ).await()
+ }
when (response) {
is NetworkResponse.Success -> {
val nextOffset = if (response.body.data.isEmpty()) null else offset + limit
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/paging/MangaRankingDataSource.kt b/app/src/main/java/com/sharkaboi/mediahub/data/paging/MangaRankingDataSource.kt
index cd14ef0..eb15a5e 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/paging/MangaRankingDataSource.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/paging/MangaRankingDataSource.kt
@@ -4,12 +4,14 @@ import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.haroldadmin.cnradapter.NetworkResponse
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.MangaRankingType
import com.sharkaboi.mediahub.data.api.models.ApiError
import com.sharkaboi.mediahub.data.api.models.manga.MangaRankingResponse
import com.sharkaboi.mediahub.data.api.retrofit.MangaService
-import com.sharkaboi.mediahub.data.wrappers.NoTokenFoundError
+import com.sharkaboi.mediahub.data.wrappers.MHError
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import timber.log.Timber
class MangaRankingDataSource(
@@ -19,14 +21,12 @@ class MangaRankingDataSource(
private val showNsfw: Boolean = false
) : PagingSource() {
+ /**
+ * prevKey == null -> first page
+ * nextKey == null -> last page
+ * both prevKey and nextKey null -> only one page
+ */
override fun getRefreshKey(state: PagingState): Int? {
- // Try to find the page key of the closest page to anchorPosition, from
- // either the prevKey or the nextKey, but you need to handle nullability
- // here:
- // * prevKey == null -> anchorPage is the first page.
- // * nextKey == null -> anchorPage is the last page.
- // * both prevKey and nextKey null -> anchorPage is the initial page, so
- // just return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(ApiConstants.API_PAGE_LIMIT) ?: anchorPage?.nextKey?.minus(
@@ -42,16 +42,18 @@ class MangaRankingDataSource(
val limit = ApiConstants.API_PAGE_LIMIT
if (accessToken == null) {
return LoadResult.Error(
- NoTokenFoundError.getThrowable()
+ MHError.LoginExpiredError.getThrowable()
)
} else {
- val response = mangaService.getMangaRankingAsync(
- authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
- offset = offset,
- limit = limit,
- rankingType = mangaRankingType.name,
- nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
- ).await()
+ val response = withContext(Dispatchers.IO) {
+ mangaService.getMangaRankingAsync(
+ authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
+ offset = offset,
+ limit = limit,
+ rankingType = mangaRankingType.name,
+ nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
+ ).await()
+ }
when (response) {
is NetworkResponse.Success -> {
val nextOffset = if (response.body.data.isEmpty()) null else offset + limit
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/paging/MangaSearchDataSource.kt b/app/src/main/java/com/sharkaboi/mediahub/data/paging/MangaSearchDataSource.kt
index 06c5971..7a12d61 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/paging/MangaSearchDataSource.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/paging/MangaSearchDataSource.kt
@@ -4,11 +4,13 @@ import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.haroldadmin.cnradapter.NetworkResponse
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.models.ApiError
import com.sharkaboi.mediahub.data.api.models.manga.MangaSearchResponse
import com.sharkaboi.mediahub.data.api.retrofit.MangaService
-import com.sharkaboi.mediahub.data.wrappers.NoTokenFoundError
+import com.sharkaboi.mediahub.data.wrappers.MHError
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import timber.log.Timber
class MangaSearchDataSource(
@@ -18,14 +20,12 @@ class MangaSearchDataSource(
private val showNsfw: Boolean = false
) : PagingSource() {
+ /**
+ * prevKey == null -> first page
+ * nextKey == null -> last page
+ * both prevKey and nextKey null -> only one page
+ */
override fun getRefreshKey(state: PagingState): Int? {
- // Try to find the page key of the closest page to anchorPosition, from
- // either the prevKey or the nextKey, but you need to handle nullability
- // here:
- // * prevKey == null -> anchorPage is the first page.
- // * nextKey == null -> anchorPage is the last page.
- // * both prevKey and nextKey null -> anchorPage is the initial page, so
- // just return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(ApiConstants.API_PAGE_LIMIT) ?: anchorPage?.nextKey?.minus(
@@ -41,16 +41,18 @@ class MangaSearchDataSource(
val limit = ApiConstants.API_PAGE_LIMIT
if (accessToken == null) {
return LoadResult.Error(
- NoTokenFoundError.getThrowable()
+ MHError.LoginExpiredError.getThrowable()
)
} else {
- val response = mangaService.getMangaAsync(
- authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
- offset = offset,
- limit = limit,
- searchQuery = query,
- nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
- ).await()
+ val response = withContext(Dispatchers.IO) {
+ mangaService.getMangaAsync(
+ authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
+ offset = offset,
+ limit = limit,
+ searchQuery = query,
+ nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
+ ).await()
+ }
when (response) {
is NetworkResponse.Success -> {
val nextOffset = if (response.body.data.isEmpty()) null else offset + limit
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/paging/UserAnimeListDataSource.kt b/app/src/main/java/com/sharkaboi/mediahub/data/paging/UserAnimeListDataSource.kt
index 30ee39f..1eb2a47 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/paging/UserAnimeListDataSource.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/paging/UserAnimeListDataSource.kt
@@ -4,13 +4,15 @@ import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.haroldadmin.cnradapter.NetworkResponse
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.AnimeStatus
import com.sharkaboi.mediahub.data.api.enums.UserAnimeSortType
import com.sharkaboi.mediahub.data.api.models.ApiError
import com.sharkaboi.mediahub.data.api.models.useranime.UserAnimeListResponse
import com.sharkaboi.mediahub.data.api.retrofit.UserAnimeService
-import com.sharkaboi.mediahub.data.wrappers.NoTokenFoundError
+import com.sharkaboi.mediahub.data.wrappers.MHError
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import timber.log.Timber
class UserAnimeListDataSource(
@@ -21,14 +23,12 @@ class UserAnimeListDataSource(
private val showNsfw: Boolean = false
) : PagingSource() {
+ /**
+ * prevKey == null -> first page
+ * nextKey == null -> last page
+ * both prevKey and nextKey null -> only one page
+ */
override fun getRefreshKey(state: PagingState): Int? {
- // Try to find the page key of the closest page to anchorPosition, from
- // either the prevKey or the nextKey, but you need to handle nullability
- // here:
- // * prevKey == null -> anchorPage is the first page.
- // * nextKey == null -> anchorPage is the last page.
- // * both prevKey and nextKey null -> anchorPage is the initial page, so
- // just return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(ApiConstants.API_PAGE_LIMIT) ?: anchorPage?.nextKey?.minus(
@@ -44,17 +44,19 @@ class UserAnimeListDataSource(
val limit = ApiConstants.API_PAGE_LIMIT
if (accessToken == null) {
return LoadResult.Error(
- NoTokenFoundError.getThrowable()
+ MHError.LoginExpiredError.getThrowable()
)
} else {
- val response = userAnimeService.getAnimeListOfUserAsync(
- authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
- status = getStatus(),
- offset = offset,
- limit = limit,
- sort = animeSortType.name,
- nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
- ).await()
+ val response = withContext(Dispatchers.IO) {
+ userAnimeService.getAnimeListOfUserAsync(
+ authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
+ status = getStatus(),
+ offset = offset,
+ limit = limit,
+ sort = animeSortType.name,
+ nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
+ ).await()
+ }
when (response) {
is NetworkResponse.Success -> {
val nextOffset = if (response.body.data.isEmpty()) null else offset + limit
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/paging/UserMangaListDataSource.kt b/app/src/main/java/com/sharkaboi/mediahub/data/paging/UserMangaListDataSource.kt
index 39176d2..1a1d0be 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/paging/UserMangaListDataSource.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/paging/UserMangaListDataSource.kt
@@ -4,13 +4,15 @@ import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.haroldadmin.cnradapter.NetworkResponse
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.MangaStatus
import com.sharkaboi.mediahub.data.api.enums.UserMangaSortType
import com.sharkaboi.mediahub.data.api.models.ApiError
import com.sharkaboi.mediahub.data.api.models.usermanga.UserMangaListResponse
import com.sharkaboi.mediahub.data.api.retrofit.UserMangaService
-import com.sharkaboi.mediahub.data.wrappers.NoTokenFoundError
+import com.sharkaboi.mediahub.data.wrappers.MHError
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import timber.log.Timber
class UserMangaListDataSource(
@@ -21,14 +23,12 @@ class UserMangaListDataSource(
private val showNsfw: Boolean = false
) : PagingSource() {
+ /**
+ * prevKey == null -> first page
+ * nextKey == null -> last page
+ * both prevKey and nextKey null -> only one page
+ */
override fun getRefreshKey(state: PagingState): Int? {
- // Try to find the page key of the closest page to anchorPosition, from
- // either the prevKey or the nextKey, but you need to handle nullability
- // here:
- // * prevKey == null -> anchorPage is the first page.
- // * nextKey == null -> anchorPage is the last page.
- // * both prevKey and nextKey null -> anchorPage is the initial page, so
- // just return null.
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(ApiConstants.API_PAGE_LIMIT) ?: anchorPage?.nextKey?.minus(
@@ -44,17 +44,19 @@ class UserMangaListDataSource(
val limit = ApiConstants.API_PAGE_LIMIT
if (accessToken == null) {
return LoadResult.Error(
- NoTokenFoundError.getThrowable()
+ MHError.LoginExpiredError.getThrowable()
)
} else {
- val response = userMangaService.getMangaListOfUserAsync(
- authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
- status = getStatus(),
- offset = offset,
- limit = limit,
- sort = mangaSortType.name,
- nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
- ).await()
+ val response = withContext(Dispatchers.IO) {
+ userMangaService.getMangaListOfUserAsync(
+ authHeader = ApiConstants.BEARER_SEPARATOR + accessToken,
+ status = getStatus(),
+ offset = offset,
+ limit = limit,
+ sort = mangaSortType.name,
+ nsfw = if (showNsfw) ApiConstants.NSFW_ALSO else ApiConstants.SFW_ONLY
+ ).await()
+ }
when (response) {
is NetworkResponse.Success -> {
val nextOffset = if (response.body.data.isEmpty()) null else offset + limit
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/sharedpref/SharedPreferencesKeys.kt b/app/src/main/java/com/sharkaboi/mediahub/data/sharedpref/SharedPreferencesKeys.kt
index 8ab42b0..dbf977a 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/sharedpref/SharedPreferencesKeys.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/sharedpref/SharedPreferencesKeys.kt
@@ -1,13 +1,11 @@
package com.sharkaboi.mediahub.data.sharedpref
-class SharedPreferencesKeys {
- companion object {
- const val DARK_MODE = "darkMode"
- const val ABOUT = "about"
- const val UPDATES = "updates"
- const val LOG_OUT = "logout"
- const val ANIME_NOTIFS = "anime_notifs"
- const val TOOLBAR = "toolbar"
- const val NSFW_OPTION = "nsfwOption"
- }
+object SharedPreferencesKeys {
+ const val DARK_MODE = "darkMode"
+ const val ABOUT = "about"
+ const val UPDATES = "updates"
+ const val LOG_OUT = "logout"
+ const val ANIME_NOTIFS = "anime_notifs"
+ const val TOOLBAR = "toolbar"
+ const val NSFW_OPTION = "nsfwOption"
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/wrappers/MHError.kt b/app/src/main/java/com/sharkaboi/mediahub/data/wrappers/MHError.kt
index 26ac8c3..03e16b1 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/data/wrappers/MHError.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/data/wrappers/MHError.kt
@@ -3,10 +3,21 @@ package com.sharkaboi.mediahub.data.wrappers
import com.sharkaboi.mediahub.common.extensions.emptyString
data class MHError(
- val errorMessage: String,
- val throwable: Throwable?
+ val errorMessage: String
) {
+ fun getThrowable() = Throwable(message = errorMessage)
+
companion object {
- val EmptyError = MHError(String.emptyString, null)
+ val EmptyError = MHError(String.emptyString)
+ val UnknownError = MHError("An unknown error occurred")
+ val InvalidStateError = MHError("An invalid state was reached")
+ val LoginExpiredError = MHError("Login expired, Log in again")
+ val NetworkError = MHError("Error with network")
+ val ParsingError = MHError("Error with parsing")
+ val ProtocolError = MHError("Protocol error")
+ val ApplicationError = MHError("Application error")
+ val AnimeNotFoundError = MHError("Anime isn't in your list")
+ val MangaNotFoundError = MHError("Manga isn't in your list")
+ fun apiErrorWithCode(code: Int) = MHError("Error with status code : $code")
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/data/wrappers/NoTokenFoundError.kt b/app/src/main/java/com/sharkaboi/mediahub/data/wrappers/NoTokenFoundError.kt
deleted file mode 100644
index e577b34..0000000
--- a/app/src/main/java/com/sharkaboi/mediahub/data/wrappers/NoTokenFoundError.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.sharkaboi.mediahub.data.wrappers
-
-class NoTokenFoundError {
- companion object {
- fun getThrowable(): Throwable {
- return Throwable(message = "Login expired, Log in again")
- }
- }
-}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/di/AlarmManagerModule.kt b/app/src/main/java/com/sharkaboi/mediahub/di/AlarmManagerModule.kt
deleted file mode 100644
index 2afc3c5..0000000
--- a/app/src/main/java/com/sharkaboi/mediahub/di/AlarmManagerModule.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.sharkaboi.mediahub.di
-
-import android.content.Context
-import com.sharkaboi.mediahub.common.alarm_manager.NotifyAlarmManager
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
-
-@InstallIn(SingletonComponent::class)
-@Module
-object AlarmManagerModule {
-
- @Provides
- @Singleton
- fun getNotifyAlarmManager(@ApplicationContext context: Context): NotifyAlarmManager =
- NotifyAlarmManager(context)
-}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime/adapters/AnimeListAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime/adapters/AnimeListAdapter.kt
index 5cc53fe..7c24ea9 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime/adapters/AnimeListAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime/adapters/AnimeListAdapter.kt
@@ -6,8 +6,9 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.extensions.getProgressStringWith
+import com.sharkaboi.mediahub.common.extensions.getRatingStringWithRating
import com.sharkaboi.mediahub.data.api.models.useranime.UserAnimeListResponse
import com.sharkaboi.mediahub.databinding.AnimeListItemBinding
@@ -22,19 +23,16 @@ class AnimeListAdapter(
fun bind(item: UserAnimeListResponse.Data?) {
item?.let {
animeListItemBinding.apply {
- ivAnimeBanner.load(it.node.mainPicture?.large ?: it.node.mainPicture?.medium) {
- crossfade(true)
- placeholder(R.drawable.ic_anime_placeholder)
- error(R.drawable.ic_anime_placeholder)
- fallback(R.drawable.ic_anime_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ ivAnimeBanner.load(
+ uri = it.node.mainPicture?.large ?: it.node.mainPicture?.medium,
+ builder = UIConstants.AnimeImageBuilder
+ )
tvAnimeName.text = it.node.title
- val numEpisodes =
- if (it.node.numTotalEpisodes == 0) "??" else it.node.numTotalEpisodes
- tvEpisodesWatched.text =
- ("${it.listStatus.numWatchedEpisodes}/$numEpisodes")
- tvScore.text = ("★ ${it.listStatus.score}")
+ tvEpisodesWatched.text = tvEpisodesWatched.context.getProgressStringWith(
+ it.listStatus.numWatchedEpisodes,
+ it.node.numTotalEpisodes
+ )
+ tvScore.text = tvScore.context.getRatingStringWithRating(it.listStatus.score)
root.setOnClickListener {
onItemClick(item.node.id)
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime/adapters/AnimePagerAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime/adapters/AnimePagerAdapter.kt
index 72a805a..3e6b777 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime/adapters/AnimePagerAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime/adapters/AnimePagerAdapter.kt
@@ -9,7 +9,7 @@ import com.sharkaboi.mediahub.modules.anime.ui.AnimeListByStatusFragment
class AnimePagerAdapter(fm: FragmentManager, lifecycle: Lifecycle) :
FragmentStateAdapter(fm, lifecycle) {
- override fun getItemCount(): Int = 6
+ override fun getItemCount(): Int = AnimeStatus.values().count()
override fun createFragment(position: Int): Fragment {
return AnimeListByStatusFragment.newInstance(AnimeStatus.values()[position])
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime/repository/AnimeRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime/repository/AnimeRepositoryImpl.kt
index d4e0083..3b041fc 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime/repository/AnimeRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime/repository/AnimeRepositoryImpl.kt
@@ -4,7 +4,7 @@ import android.content.SharedPreferences
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.AnimeStatus
import com.sharkaboi.mediahub.data.api.enums.UserAnimeSortType
import com.sharkaboi.mediahub.data.api.models.useranime.UserAnimeListResponse
@@ -12,8 +12,10 @@ import com.sharkaboi.mediahub.data.api.retrofit.UserAnimeService
import com.sharkaboi.mediahub.data.datastore.DataStoreRepository
import com.sharkaboi.mediahub.data.paging.UserAnimeListDataSource
import com.sharkaboi.mediahub.data.sharedpref.SharedPreferencesKeys
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.withContext
import timber.log.Timber
class AnimeRepositoryImpl(
@@ -25,11 +27,11 @@ class AnimeRepositoryImpl(
override suspend fun getAnimeListFlow(
animeStatus: AnimeStatus,
animeSortType: UserAnimeSortType
- ): Flow> {
+ ): Flow> = withContext(Dispatchers.IO) {
val showNsfw = sharedPreferences.getBoolean(SharedPreferencesKeys.NSFW_OPTION, false)
val accessToken: String? = dataStoreRepository.accessTokenFlow.firstOrNull()
Timber.d("accessToken: $accessToken")
- return Pager(
+ return@withContext Pager(
config = PagingConfig(
pageSize = ApiConstants.API_PAGE_LIMIT,
enablePlaceholders = false
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime/ui/AnimeFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime/ui/AnimeFragment.kt
index 42fa2ce..68e5342 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime/ui/AnimeFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime/ui/AnimeFragment.kt
@@ -47,7 +47,7 @@ class AnimeFragment : Fragment() {
binding.vpAnime.adapter = vpAnimeAdapter
binding.animeTabLayout.apply {
AnimeStatus.values().forEach {
- addTab(newTab().apply { text = it.getFormattedString() })
+ addTab(newTab().apply { text = it.getFormattedString(context) })
}
}
onTabChanged = object : TabLayout.OnTabSelectedListener {
@@ -59,6 +59,7 @@ class AnimeFragment : Fragment() {
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
override fun onTabReselected(tab: TabLayout.Tab?) {
+ // Scroll RV on tab reselect and bring up bottom nav
childFragmentManager.findFragmentByTag("f${tab?.position ?: 0}")?.let {
if (it is AnimeListByStatusFragment) {
it.scrollRecyclerView()
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime/ui/AnimeListByStatusFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime/ui/AnimeListByStatusFragment.kt
index 36a29a9..9d9009d 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime/ui/AnimeListByStatusFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime/ui/AnimeListByStatusFragment.kt
@@ -13,6 +13,9 @@ import androidx.paging.LoadState
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.sharkaboi.mediahub.BottomNavGraphDirections
+import com.sharkaboi.mediahub.R
+import com.sharkaboi.mediahub.common.constants.UIConstants
import com.sharkaboi.mediahub.common.extensions.showToast
import com.sharkaboi.mediahub.data.api.enums.AnimeStatus
import com.sharkaboi.mediahub.data.api.enums.UserAnimeSortType
@@ -74,10 +77,10 @@ class AnimeListByStatusFragment : Fragment() {
private fun setUpRecyclerView() {
binding.rvAnimeByStatus.apply {
animeListAdapter = AnimeListAdapter { animeId ->
- val action = AnimeFragmentDirections.openAnimeDetailsWithId(animeId)
+ val action = BottomNavGraphDirections.openAnimeById(animeId)
navController.navigate(action)
}
- layoutManager = GridLayoutManager(context, 3)
+ layoutManager = GridLayoutManager(context, UIConstants.AnimeAndMangaGridSpanCount)
itemAnimator = DefaultItemAnimator()
adapter = animeListAdapter.withLoadStateFooter(
footer = AnimeLoadStateAdapter()
@@ -110,10 +113,10 @@ class AnimeListByStatusFragment : Fragment() {
}
private fun openSortMenu() {
- val singleItems = UserAnimeSortType.getFormattedArray()
+ val singleItems = UserAnimeSortType.getFormattedArray(requireContext())
val checkedItem = UserAnimeSortType.values().indexOf(animeViewModel.currentChosenSortType)
MaterialAlertDialogBuilder(requireContext())
- .setTitle("Sort anime by")
+ .setTitle(R.string.sort_anime_by_hint)
.setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
animeViewModel.setSortType(UserAnimeSortType.values()[which])
getAnimeList()
@@ -122,7 +125,7 @@ class AnimeListByStatusFragment : Fragment() {
.show()
}
- fun getAnimeList() {
+ private fun getAnimeList() {
resultsJob?.cancel()
resultsJob = lifecycleScope.launch {
animeViewModel.getAnimeList().collectLatest { pagingData ->
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/adapters/RecommendedAnimeAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/adapters/RecommendedAnimeAdapter.kt
index dce120b..a86db40 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/adapters/RecommendedAnimeAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/adapters/RecommendedAnimeAdapter.kt
@@ -7,8 +7,8 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
import com.sharkaboi.mediahub.R
+import com.sharkaboi.mediahub.common.constants.UIConstants
import com.sharkaboi.mediahub.data.api.models.anime.AnimeByIDResponse
import com.sharkaboi.mediahub.databinding.AnimeListItemBinding
@@ -62,17 +62,16 @@ class RecommendedAnimeAdapter(private val onClick: (Int) -> Unit) :
}
binding.tvAnimeName.text = item.node.title
binding.tvEpisodesWatched.text =
- ("Recommended ${item.numRecommendations} ${if (item.numRecommendations == 1) "time" else "times"}")
+ binding.tvEpisodesWatched.context.resources.getQuantityString(
+ R.plurals.recommendation_times,
+ item.numRecommendations,
+ item.numRecommendations
+ )
binding.cardRating.isGone = true
binding.ivAnimeBanner.load(
- item.node.mainPicture?.large ?: item.node.mainPicture?.medium
- ) {
- crossfade(true)
- placeholder(R.drawable.ic_anime_placeholder)
- error(R.drawable.ic_anime_placeholder)
- fallback(R.drawable.ic_anime_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ uri = item.node.mainPicture?.large ?: item.node.mainPicture?.medium,
+ builder = UIConstants.AnimeImageBuilder
+ )
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/adapters/RelatedAnimeAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/adapters/RelatedAnimeAdapter.kt
index cf48bbd..420e36a 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/adapters/RelatedAnimeAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/adapters/RelatedAnimeAdapter.kt
@@ -7,8 +7,7 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
+import com.sharkaboi.mediahub.common.constants.UIConstants
import com.sharkaboi.mediahub.data.api.models.anime.AnimeByIDResponse
import com.sharkaboi.mediahub.databinding.AnimeListItemBinding
@@ -64,14 +63,9 @@ class RelatedAnimeAdapter(private val onClick: (Int) -> Unit) :
binding.tvEpisodesWatched.text = item.relationTypeFormatted
binding.cardRating.isGone = true
binding.ivAnimeBanner.load(
- item.node.mainPicture?.large ?: item.node.mainPicture?.medium
- ) {
- crossfade(true)
- placeholder(R.drawable.ic_anime_placeholder)
- error(R.drawable.ic_anime_placeholder)
- fallback(R.drawable.ic_anime_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ uri = item.node.mainPicture?.large ?: item.node.mainPicture?.medium,
+ builder = UIConstants.AnimeImageBuilder
+ )
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/adapters/RelatedMangaAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/adapters/RelatedMangaAdapter.kt
index 444bb61..09acc8d 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/adapters/RelatedMangaAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/adapters/RelatedMangaAdapter.kt
@@ -7,8 +7,7 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
+import com.sharkaboi.mediahub.common.constants.UIConstants
import com.sharkaboi.mediahub.data.api.models.anime.AnimeByIDResponse
import com.sharkaboi.mediahub.databinding.MangaListItemBinding
@@ -64,14 +63,9 @@ class RelatedMangaAdapter(private val onClick: (Int) -> Unit) :
binding.tvChapsRead.text = item.relationTypeFormatted
binding.cardRating.isGone = true
binding.ivMangaBanner.load(
- item.node.mainPicture?.large ?: item.node.mainPicture?.medium
- ) {
- crossfade(true)
- placeholder(R.drawable.ic_manga_placeholder)
- error(R.drawable.ic_manga_placeholder)
- fallback(R.drawable.ic_manga_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ uri = item.node.mainPicture?.large ?: item.node.mainPicture?.medium,
+ builder = UIConstants.MangaImageBuilder
+ )
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/repository/AnimeDetailsRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/repository/AnimeDetailsRepositoryImpl.kt
index fb2c97e..3a8ea70 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/repository/AnimeDetailsRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/repository/AnimeDetailsRepositoryImpl.kt
@@ -6,8 +6,7 @@ import com.apollographql.apollo.coroutines.await
import com.apollographql.apollo.exception.ApolloException
import com.haroldadmin.cnradapter.NetworkResponse
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.common.extensions.ifNullOrBlank
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.models.anime.AnimeByIDResponse
import com.sharkaboi.mediahub.data.api.retrofit.AnimeService
import com.sharkaboi.mediahub.data.api.retrofit.UserAnimeService
@@ -17,6 +16,7 @@ import com.sharkaboi.mediahub.data.wrappers.MHTaskState
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.firstOrNull
import timber.log.Timber
+import java.util.*
class AnimeDetailsRepositoryImpl(
private val animeService: AnimeService,
@@ -33,7 +33,7 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError("Log in has expired, Log in again.", null)
+ error = MHError.LoginExpiredError
)
} else {
val result = animeService.getAnimeByIdAsync(
@@ -54,10 +54,8 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with network" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.NetworkError
)
}
is NetworkResponse.ServerError -> {
@@ -65,10 +63,8 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.body?.message.ifNullOrBlank { "Error with status code : ${result.code}" },
- null
- )
+ error = result.body?.message?.let { MHError(it) }
+ ?: MHError.apiErrorWithCode(result.code)
)
}
is NetworkResponse.UnknownError -> {
@@ -76,10 +72,8 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with parsing" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.ParsingError
)
}
}
@@ -90,7 +84,7 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(e.message.ifNullOrBlank { "Unknown error" }, e)
+ error = e.message?.let { MHError(it) } ?: MHError.UnknownError
)
}
}
@@ -103,7 +97,7 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(e.message.ifNullOrBlank { "Protocol error" }, e.cause)
+ error = e.message?.let { MHError(it) } ?: MHError.ProtocolError
)
}
@@ -113,7 +107,7 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(errorMessage.ifNullOrBlank { "Application error" }, null)
+ error = errorMessage?.let { MHError(it) } ?: MHError.ApplicationError
)
} else {
return@withContext MHTaskState(
@@ -137,7 +131,7 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError("Log in has expired, Log in again.", null)
+ error = MHError.LoginExpiredError
)
} else {
val result = userAnimeService.updateAnimeStatusAsync(
@@ -161,10 +155,8 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with network" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.NetworkError
)
}
is NetworkResponse.ServerError -> {
@@ -172,10 +164,8 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.body?.message.ifNullOrBlank { "Error with status code : ${result.code}" },
- null
- )
+ error = result.body?.message?.let { MHError(it) }
+ ?: MHError.apiErrorWithCode(result.code)
)
}
is NetworkResponse.UnknownError -> {
@@ -183,10 +173,8 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with parsing" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.ParsingError
)
}
}
@@ -197,7 +185,7 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(e.message.ifNullOrBlank { "Unknown Error" }, e)
+ error = e.message?.let { MHError(it) } ?: MHError.UnknownError
)
}
}
@@ -212,7 +200,7 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError("Log in has expired, Log in again.", null)
+ error = MHError.LoginExpiredError
)
} else {
val result = userAnimeService.deleteAnimeFromListAsync(
@@ -233,10 +221,8 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with network" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.NetworkError
)
}
is NetworkResponse.ServerError -> {
@@ -245,18 +231,14 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- "Anime isn't in your list", null
- )
+ error = MHError.AnimeNotFoundError
)
}
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.body?.message.ifNullOrBlank { "Error with status code : ${result.code}" },
- null
- )
+ error = result.body?.message?.let { MHError(it) }
+ ?: MHError.apiErrorWithCode(result.code)
)
}
is NetworkResponse.UnknownError -> {
@@ -264,10 +246,8 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with parsing" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.ParsingError
)
}
}
@@ -278,7 +258,7 @@ class AnimeDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(e.message.ifNullOrBlank { "Unknown error" }, e)
+ error = e.message?.let { MHError(it) } ?: MHError.UnknownError
)
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/ui/AnimeDetailsFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/ui/AnimeDetailsFragment.kt
index 7973aa0..ed294cb 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/ui/AnimeDetailsFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/ui/AnimeDetailsFragment.kt
@@ -19,15 +19,19 @@ import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import coil.load
-import coil.transform.RoundedCornersTransformation
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.shape.ShapeAppearanceModel
+import com.sharkaboi.mediahub.BottomNavGraphDirections
import com.sharkaboi.mediahub.R
-import com.sharkaboi.mediahub.common.constants.MALExternalLinks
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.constants.UIConstants.setMediaHubChipStyle
import com.sharkaboi.mediahub.common.extensions.*
import com.sharkaboi.mediahub.common.util.openUrl
+import com.sharkaboi.mediahub.data.api.constants.MALExternalLinks
+import com.sharkaboi.mediahub.data.api.enums.AnimeRating.getAnimeRating
import com.sharkaboi.mediahub.data.api.enums.AnimeStatus
+import com.sharkaboi.mediahub.data.api.enums.getAnimeAiringStatus
+import com.sharkaboi.mediahub.data.api.enums.getAnimeNsfwRating
import com.sharkaboi.mediahub.data.api.models.anime.AnimeByIDResponse
import com.sharkaboi.mediahub.databinding.CustomEpisodeCountDialogBinding
import com.sharkaboi.mediahub.databinding.FragmentAnimeDetailsBinding
@@ -112,17 +116,10 @@ class AnimeDetailsFragment : Fragment() {
private val handleListStatusUpdate = { state: AnimeDetailsUpdateClass ->
binding.animeDetailsUserListCard.apply {
btnStatus.text =
- state.animeStatus?.getFormattedString()
+ state.animeStatus?.getFormattedString(requireContext())
?: getString(R.string.not_added)
- btnScore.text = ("${state.score ?: 0}/10")
- btnCount.text = (
- "${state.numWatchedEpisode ?: 0}/${
- if (state.totalEps == 0)
- "??"
- else
- state.totalEps.toString()
- }"
- )
+ btnScore.text = getString(R.string.media_rating_template, state.score ?: 0)
+ btnCount.text = context?.getProgressStringWith(state.numWatchedEpisode, state.totalEps)
btnScore.setOnClickListener {
openScoreDialog(state.score)
}
@@ -144,172 +141,104 @@ class AnimeDetailsFragment : Fragment() {
if (nextEp == null) {
binding.nextEpisodeDetails.root.isGone = true
} else {
- val timeFromNow = nextEp.timeUntilAiring.getAiringTimeFormatted()
- val airingString = buildString {
- append("Episode ")
- append(nextEp.episode)
- append(" airs in ")
- append(timeFromNow)
- }
- binding.nextEpisodeDetails.tvNextEpisodeDetails.text = airingString
+ binding.nextEpisodeDetails.tvNextEpisodeDetails.text =
+ context?.getAiringTimeFormatted(nextEp)
}
}
private fun setData(animeByIDResponse: AnimeByIDResponse) {
- binding.apply {
- tvAnimeName.text = animeByIDResponse.title
- tvAlternateTitles.setOnClickListener {
- showAlternateTitlesDialog(animeByIDResponse.alternativeTitles)
- }
- tvStartDate.text =
- animeByIDResponse.startDate?.tryParseDate()?.formatDateDMY()
- ?: getString(R.string.n_a)
- tvEndDate.text =
- animeByIDResponse.endDate?.tryParseDate()?.formatDateDMY()
- ?: getString(R.string.n_a)
- tvMeanScore.text = animeByIDResponse.mean?.toString() ?: getString(R.string.n_a)
- tvRank.text = animeByIDResponse.rank?.toString() ?: getString(R.string.n_a)
- tvPopularityRank.text = animeByIDResponse.popularity.toString()
- studiosChipGroup.apply {
- removeAllViews()
- if (animeByIDResponse.studios.isEmpty()) {
- addView(
- TextView(context).apply {
- text = getString(R.string.n_a)
- }
- )
- } else {
- animeByIDResponse.studios.forEach { studio ->
- addView(
- TextView(context).apply {
- setTextColor(ContextCompat.getColor(context, R.color.colorPrimary))
- setTypeface(null, Typeface.BOLD)
- text = ("${studio.name} ")
- setOnClickListener {
- openUrl(
- MALExternalLinks.getAnimeProducerPageLink(studio)
- )
- }
- }
- )
- }
- }
- }
- ivAnimeMainPicture.load(
- animeByIDResponse.mainPicture?.large ?: animeByIDResponse.mainPicture?.medium
- ) {
- crossfade(true)
- placeholder(R.drawable.ic_anime_placeholder)
- error(R.drawable.ic_anime_placeholder)
- fallback(R.drawable.ic_anime_placeholder)
- transformations(RoundedCornersTransformation(8f))
+ setupAnimeMainDetailLayout(animeByIDResponse)
+ setupAnimeUserListStatusCard(animeByIDResponse)
+ setupAnimeOtherDetailLayout(animeByIDResponse)
+ setupAnimeOtherDetailsCard(animeByIDResponse)
+ setupAnimeOtherDetailsButtons(animeByIDResponse)
+ setupAnimeRecommendationsList(animeByIDResponse.recommendations)
+ setupRelatedAnimeList(animeByIDResponse.relatedAnime)
+ setupRelatedMangaList(animeByIDResponse.relatedManga)
+ }
+
+ private fun setupAnimeUserListStatusCard(animeByIDResponse: AnimeByIDResponse) =
+ binding.animeDetailsUserListCard.apply {
+ btnPlus1.setOnClickListener {
+ animeDetailsViewModel.add1ToWatchedEps()
}
- ivAnimeMainPicture.setOnClickListener {
- openImagesViewPager(animeByIDResponse.pictures)
+ btnPlus5.setOnClickListener {
+ animeDetailsViewModel.add5ToWatchedEps()
}
- otherDetails.tvSynopsis.text =
- animeByIDResponse.synopsis?.ifBlank { getString(R.string.n_a) }
- ?: getString(R.string.n_a)
- otherDetails.tvSynopsis.setOnClickListener {
- showFullSynopsisDialog(
- animeByIDResponse.synopsis?.ifBlank { getString(R.string.n_a) }
- ?: getString(R.string.n_a)
- )
+ btnPlus10.setOnClickListener {
+ animeDetailsViewModel.add10ToWatchedEps()
}
- otherDetails.ratingsChipGroup.apply {
- removeAllViews()
- addView(
- Chip(context).apply {
- setEnsureMinTouchTargetSize(false)
- shapeAppearanceModel = ShapeAppearanceModel().withCornerSize(8f)
- text =
- animeByIDResponse.nsfw?.getAnimeNsfwRating() ?: getString(R.string.n_a)
- }
- )
- addView(
- Chip(context).apply {
- setEnsureMinTouchTargetSize(false)
- shapeAppearanceModel = ShapeAppearanceModel().withCornerSize(8f)
- text = animeByIDResponse.rating?.getRating() ?: getString(R.string.n_a)
- }
+ btnCount.setOnClickListener {
+ openAnimeWatchedCountDialog(
+ animeByIDResponse.numEpisodes,
+ animeByIDResponse.myListStatus?.numEpisodesWatched
)
}
- animeByIDResponse.genres.let {
- otherDetails.genresChipGroup.removeAllViews()
- if (it.isEmpty()) {
- otherDetails.genresChipGroup.addView(
- Chip(context).apply {
- setEnsureMinTouchTargetSize(false)
- shapeAppearanceModel = ShapeAppearanceModel().withCornerSize(8f)
- text = getString(R.string.n_a)
- }
- )
- } else {
- it.forEach { genre ->
- otherDetails.genresChipGroup.addView(
- Chip(context).apply {
- setEnsureMinTouchTargetSize(false)
- setOnClickListener {
- openUrl(
- MALExternalLinks.getAnimeGenresLink(
- genre
- )
- )
- }
- shapeAppearanceModel = ShapeAppearanceModel().withCornerSize(8f)
- text = genre.name
- }
- )
- }
- }
- }
- otherDetails.btnMediaType.text = ("Type : ${animeByIDResponse.mediaType.uppercase()}")
- otherDetails.btnMediaType.setOnClickListener {
- val action =
- AnimeDetailsFragmentDirections.openAnimeRankings(animeByIDResponse.mediaType)
- navController.navigate(action)
- }
- otherDetails.btnAnimeCurrentStatus.text =
- animeByIDResponse.status.getAnimeAiringStatus()
- otherDetails.btnTotalEps.text =
- animeByIDResponse.numEpisodes.let {
- if (it == 0)
- "${getString(R.string.n_a)} eps"
- else
- "$it ${if (it == 1) "ep" else "eps"}"
- }
- otherDetails.btnSeason.text = animeByIDResponse.startSeason?.let {
- "${it.season.capitalizeFirst()} ${it.year}"
- } ?: "Season : ${getString(R.string.n_a)}"
- otherDetails.btnSeason.setOnClickListener {
- val action =
- AnimeDetailsFragmentDirections.openAnimeSeasonals(
- animeByIDResponse.startSeason?.season,
- animeByIDResponse.startSeason?.year ?: 0
- )
- navController.navigate(action)
+ btnScore.setOnClickListener {
+ openScoreDialog(animeByIDResponse.myListStatus?.score)
}
- otherDetails.tvSchedule.text =
- animeByIDResponse.broadcast?.getBroadcastTime() ?: getString(R.string.n_a)
- otherDetails.btnSource.text =
- (
- "From ${
- animeByIDResponse.source?.replaceUnderScoreWithWhiteSpace()
- ?.capitalizeFirst()
- ?: getString(R.string.n_a)
- }"
- )
- otherDetails.btnAverageLength.text =
- animeByIDResponse.averageEpisodeDuration.getEpisodeLengthFromSeconds()
- otherDetails.ibNotify.setOnClickListener {
- onNotifyClick(animeByIDResponse.id, animeByIDResponse.broadcast)
+ btnStatus.setOnClickListener {
+ openStatusDialog(animeByIDResponse.myListStatus?.status, animeByIDResponse.id)
}
- otherDetails.chipGroupOptions.forEach {
- if (it is Chip) {
- it.setEnsureMinTouchTargetSize(false)
- it.shapeAppearanceModel = ShapeAppearanceModel().withCornerSize(8f)
- }
+ btnConfirm.setOnClickListener {
+ animeDetailsViewModel.submitStatusUpdate(animeByIDResponse.id)
}
+ }
+
+ private fun setupRelatedMangaList(relatedManga: List) {
+ val rvRelatedManga = binding.otherDetails.rvRelatedManga
+ rvRelatedManga.adapter = RelatedMangaAdapter { mangaId ->
+ openMangaWithId(mangaId)
+ }.apply {
+ submitList(relatedManga)
+ }
+ binding.otherDetails.tvRelatedMangaHint.isVisible = relatedManga.isNotEmpty()
+ rvRelatedManga.layoutManager =
+ LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ rvRelatedManga.setHasFixedSize(true)
+ rvRelatedManga.itemAnimator = DefaultItemAnimator()
+ }
+
+ private fun openMangaWithId(mangaId: Int) {
+ val action = BottomNavGraphDirections.openMangaById(mangaId)
+ navController.navigate(action)
+ }
+
+ private fun setupRelatedAnimeList(relatedAnime: List) {
+ val rvRelatedAnime = binding.otherDetails.rvRelatedAnime
+ rvRelatedAnime.adapter = RelatedAnimeAdapter { animeId ->
+ openAnimeWithId(animeId)
+ }.apply {
+ submitList(relatedAnime)
+ }
+ binding.otherDetails.tvRelatedAnimeHint.isVisible = relatedAnime.isNotEmpty()
+ rvRelatedAnime.layoutManager =
+ LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ rvRelatedAnime.setHasFixedSize(true)
+ rvRelatedAnime.itemAnimator = DefaultItemAnimator()
+ }
+
+ private fun setupAnimeRecommendationsList(recommendations: List) {
+ val rvRecommendations = binding.otherDetails.rvRecommendations
+ rvRecommendations.adapter = RecommendedAnimeAdapter { animeId ->
+ openAnimeWithId(animeId)
+ }.apply {
+ submitList(recommendations)
+ }
+ binding.otherDetails.tvRecommendations.isVisible = recommendations.isNotEmpty()
+ rvRecommendations.layoutManager =
+ LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ rvRecommendations.setHasFixedSize(true)
+ rvRecommendations.itemAnimator = DefaultItemAnimator()
+ }
+
+ private fun openAnimeWithId(animeId: Int) {
+ val action = BottomNavGraphDirections.openAnimeById(animeId)
+ navController.navigate(action)
+ }
+
+ private fun setupAnimeOtherDetailsButtons(animeByIDResponse: AnimeByIDResponse) =
+ binding.apply {
otherDetails.btnBackground.setOnClickListener {
openBackgroundDialog(animeByIDResponse.background)
}
@@ -356,104 +285,171 @@ class AnimeDetailsFragment : Fragment() {
otherDetails.btnStatistics.setOnClickListener {
openStatsDialog(animeByIDResponse.statistics)
}
- otherDetails.rvRecommendations.apply {
- adapter = RecommendedAnimeAdapter { animeId ->
- val action = AnimeDetailsFragmentDirections.animeDetailsWithId(animeId)
- navController.navigate(action)
- }.apply {
- submitList(animeByIDResponse.recommendations)
- }
- otherDetails.tvRecommendations.isVisible =
- animeByIDResponse.recommendations.isNotEmpty()
- layoutManager =
- LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- setHasFixedSize(true)
- itemAnimator = DefaultItemAnimator()
- }
- otherDetails.rvRelatedAnime.apply {
- adapter = RelatedAnimeAdapter { animeId ->
- val action = AnimeDetailsFragmentDirections.animeDetailsWithId(animeId)
- navController.navigate(action)
- }.apply {
- submitList(animeByIDResponse.relatedAnime)
- }
- otherDetails.tvRelatedAnimeHint.isVisible =
- animeByIDResponse.relatedAnime.isNotEmpty()
- layoutManager =
- LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- setHasFixedSize(true)
- itemAnimator = DefaultItemAnimator()
+ }
+
+ private fun setupAnimeOtherDetailsCard(animeByIDResponse: AnimeByIDResponse) = binding.apply {
+ otherDetails.btnMediaType.text =
+ context?.getMediaTypeStringWith(animeByIDResponse.mediaType.capitalizeFirst())
+ otherDetails.btnMediaType.setOnClickListener {
+ openAnimeRankingWith(animeByIDResponse.mediaType)
+ }
+ otherDetails.btnAnimeCurrentStatus.text =
+ context?.getAnimeAiringStatus(animeByIDResponse.status)
+ otherDetails.btnTotalEps.text =
+ context?.getEpisodesOfAnimeString(animeByIDResponse.numEpisodes)
+ otherDetails.btnSeason.text = context?.getAnimeSeasonString(
+ animeByIDResponse.startSeason?.season,
+ animeByIDResponse.startSeason?.year
+ )
+ otherDetails.btnSeason.setOnClickListener {
+ openAnimeSeasonalWith(
+ animeByIDResponse.startSeason?.season,
+ animeByIDResponse.startSeason?.year ?: 0
+ )
+ }
+ otherDetails.tvSchedule.text =
+ context?.getAnimeBroadcastTime(animeByIDResponse.broadcast)
+ otherDetails.btnSource.text =
+ context?.getAnimeOriginalSourceString(animeByIDResponse.source)
+ otherDetails.btnAverageLength.text =
+ context?.getEpisodeLengthFromSeconds(animeByIDResponse.averageEpisodeDuration)
+ otherDetails.ibNotify.setOnClickListener {
+ onNotifyClick(animeByIDResponse.id, animeByIDResponse.broadcast)
+ }
+ otherDetails.chipGroupOptions.forEach {
+ if (it is Chip) {
+ it.setMediaHubChipStyle()
}
- otherDetails.rvRelatedManga.apply {
- adapter = RelatedMangaAdapter { mangaId ->
- val action = AnimeDetailsFragmentDirections.openMangaDetailsWithId(mangaId)
- navController.navigate(action)
- }.apply {
- submitList(animeByIDResponse.relatedManga)
+ }
+ }
+
+ private fun openAnimeSeasonalWith(season: String?, year: Int) {
+ val action =
+ AnimeDetailsFragmentDirections.openAnimeSeasonals(season, year)
+ navController.navigate(action)
+ }
+
+ private fun openAnimeRankingWith(mediaType: String) {
+ val action =
+ AnimeDetailsFragmentDirections.openAnimeRankings(mediaType)
+ navController.navigate(action)
+ }
+
+ private fun setupAnimeOtherDetailLayout(animeByIDResponse: AnimeByIDResponse) {
+ binding.otherDetails.tvSynopsis.text =
+ animeByIDResponse.synopsis.ifNullOrBlank { getString(R.string.n_a) }
+ binding.otherDetails.tvSynopsis.setOnClickListener {
+ showFullSynopsisDialog(animeByIDResponse.synopsis)
+ }
+ setupRatingsChipGroup(animeByIDResponse)
+ setupGenresChipGroup(animeByIDResponse.genres)
+ }
+
+ private fun setupGenresChipGroup(genres: List) {
+ val chipGroup = binding.otherDetails.genresChipGroup
+ chipGroup.removeAllViews()
+ if (genres.isEmpty()) {
+ val naChip = Chip(context)
+ naChip.setMediaHubChipStyle()
+ naChip.text = getString(R.string.n_a)
+ chipGroup.addView(naChip)
+ } else {
+ genres.forEach { genre ->
+ val genreChip = Chip(context)
+ genreChip.setMediaHubChipStyle()
+ genreChip.text = genre.name
+ genreChip.setOnClickListener {
+ openUrl(MALExternalLinks.getAnimeGenresLink(genre))
}
- otherDetails.tvRelatedMangaHint.isVisible =
- animeByIDResponse.relatedManga.isNotEmpty()
- layoutManager =
- LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- setHasFixedSize(true)
- itemAnimator = DefaultItemAnimator()
+ chipGroup.addView(genreChip)
}
- animeDetailsUserListCard.apply {
- btnPlus1.setOnClickListener {
- animeDetailsViewModel.add1ToWatchedEps()
- }
- btnPlus5.setOnClickListener {
- animeDetailsViewModel.add5ToWatchedEps()
- }
- btnPlus10.setOnClickListener {
- animeDetailsViewModel.add10ToWatchedEps()
- }
- btnCount.setOnClickListener {
- openAnimeWatchedCountDialog(
- animeByIDResponse.numEpisodes,
- animeByIDResponse.myListStatus?.numEpisodesWatched
- )
- }
- btnScore.setOnClickListener {
- openScoreDialog(animeByIDResponse.myListStatus?.score)
- }
- btnStatus.setOnClickListener {
- openStatusDialog(animeByIDResponse.myListStatus?.status, animeByIDResponse.id)
- }
- btnConfirm.setOnClickListener {
- animeDetailsViewModel.submitStatusUpdate(animeByIDResponse.id)
+ }
+ }
+
+ private fun setupAnimeMainDetailLayout(animeByIDResponse: AnimeByIDResponse) = binding.apply {
+ setupAnimeImagePreview(animeByIDResponse)
+ tvAnimeName.text = animeByIDResponse.title
+ tvAlternateTitles.setOnClickListener {
+ showAlternateTitlesDialog(animeByIDResponse.alternativeTitles)
+ }
+ tvStartDate.text =
+ animeByIDResponse.startDate?.tryParseDate()?.formatDateDMY()
+ ?: getString(R.string.n_a)
+ tvEndDate.text =
+ animeByIDResponse.endDate?.tryParseDate()?.formatDateDMY()
+ ?: getString(R.string.n_a)
+ tvMeanScore.text = animeByIDResponse.mean?.toString() ?: getString(R.string.n_a)
+ tvRank.text = animeByIDResponse.rank?.toString() ?: getString(R.string.n_a)
+ tvPopularityRank.text = animeByIDResponse.popularity.toString()
+ setupStudiosChipGroup(animeByIDResponse.studios)
+ }
+
+ private fun setupRatingsChipGroup(animeByIDResponse: AnimeByIDResponse) {
+ val chipGroup = binding.otherDetails.ratingsChipGroup
+ chipGroup.removeAllViews()
+ val nsfwRatingChip = Chip(context)
+ nsfwRatingChip.setMediaHubChipStyle()
+ nsfwRatingChip.text = nsfwRatingChip.context.getAnimeNsfwRating(animeByIDResponse.nsfw)
+ chipGroup.addView(nsfwRatingChip)
+ val pgRatingChip = Chip(context)
+ pgRatingChip.setMediaHubChipStyle()
+ pgRatingChip.text = pgRatingChip.context.getAnimeRating(animeByIDResponse.rating)
+ chipGroup.addView(pgRatingChip)
+ }
+
+ private fun setupAnimeImagePreview(animeByIDResponse: AnimeByIDResponse) {
+ binding.ivAnimeMainPicture.load(
+ uri = animeByIDResponse.mainPicture?.large ?: animeByIDResponse.mainPicture?.medium,
+ builder = UIConstants.AnimeImageBuilder
+ )
+ binding.ivAnimeMainPicture.setOnClickListener {
+ openImagesViewPager(animeByIDResponse.pictures)
+ }
+ }
+
+ private fun setupStudiosChipGroup(studios: List) {
+ binding.studiosChipGroup.removeAllViews()
+ if (studios.isEmpty()) {
+ val textView = TextView(context)
+ textView.text = getString(R.string.n_a)
+ binding.studiosChipGroup.addView(textView)
+ } else {
+ studios.forEach { studio ->
+ val textView = TextView(context)
+ textView.setTextColor(
+ ContextCompat.getColor(textView.context, R.color.colorPrimary)
+ )
+ textView.setTypeface(null, Typeface.BOLD)
+ textView.text = studio.name.plus(" ")
+ textView.setOnClickListener {
+ openUrl(MALExternalLinks.getAnimeProducerPageLink(studio))
}
+ binding.studiosChipGroup.addView(textView)
}
}
}
private fun onNotifyClick(id: Int, broadcast: AnimeByIDResponse.Broadcast?) {
- showToast("Coming soon!")
+ showToast(R.string.coming_soon_hint)
}
private fun openImagesViewPager(pictures: List) {
val images: List = pictures.map { it.large ?: it.medium }
- val action = AnimeDetailsFragmentDirections.openImages(images.toTypedArray())
+ val action = BottomNavGraphDirections.openImageSlider(images.toTypedArray())
navController.navigate(action)
}
- private fun showFullSynopsisDialog(synopsis: String) {
- MaterialAlertDialogBuilder(requireContext())
- .setTitle("Synopsis")
- .setMessage(
- synopsis
- ).setPositiveButton(android.R.string.ok) { dialog, _ ->
- dialog.dismiss()
- }.show()
- }
+ private fun showFullSynopsisDialog(synopsis: String?) =
+ requireContext().showNoActionOkDialog(R.string.synopsis, synopsis)
private fun openStatusDialog(status: String?, animeId: Int) {
- val singleItems =
- arrayOf("Not added") + AnimeStatus.malStatuses.map { it.getFormattedString() }
+ val singleItems = arrayOf(getString(R.string.not_added)) + AnimeStatus.malStatuses.map {
+ it.getFormattedString(requireContext())
+ }
val checkedItem =
status?.let { AnimeStatus.malStatuses.indexOfFirst { it.name == status } + 1 } ?: 0
MaterialAlertDialogBuilder(requireContext())
- .setTitle("Set status as")
+ .setTitle(R.string.media_set_status_hint)
.setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
when (which) {
checkedItem -> Unit
@@ -469,79 +465,69 @@ class AnimeDetailsFragment : Fragment() {
val singleItems = (0..10).map { it.toString() }.toTypedArray()
val checkedItem = score ?: 0
MaterialAlertDialogBuilder(requireContext())
- .setTitle("Set score as")
+ .setTitle(R.string.media_set_score_hint)
.setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
when (which) {
checkedItem -> Unit
else -> animeDetailsViewModel.setScore(which)
}
dialog.dismiss()
- }
- .show()
+ }.show()
}
- @SuppressLint("InflateParams")
private fun openAnimeWatchedCountDialog(totalEps: Int?, watchedEps: Int?) {
if (totalEps == null || totalEps == 0) {
- val view =
- LayoutInflater.from(context).inflate(R.layout.custom_episode_count_dialog, null)
- val binding: CustomEpisodeCountDialogBinding =
- CustomEpisodeCountDialogBinding.bind(view)
- binding.etNum.setText(watchedEps?.toString() ?: "")
- MaterialAlertDialogBuilder(requireContext())
- .setView(binding.root)
- .setPositiveButton("Ok") { dialog, _ ->
- val count = binding.etNum.text?.toString()?.toInt() ?: 0
- animeDetailsViewModel.setEpisodeCount(count)
- dialog.dismiss()
- }
- .setNegativeButton("Cancel") { dialog, _ ->
- dialog.dismiss()
- }
- .show()
+ showWatchedCountDialogWithTextField(watchedEps)
} else {
- val singleItems = (0..totalEps).map { it.toString() }.toTypedArray()
- val checkedItem = watchedEps ?: 0
- MaterialAlertDialogBuilder(requireContext())
- .setTitle("Watched till episode")
- .setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
- animeDetailsViewModel.setEpisodeCount(which)
- dialog.dismiss()
- }
- .show()
+ showWatchedCountDialogWithList(totalEps, watchedEps)
}
}
- private fun openBackgroundDialog(background: String?) {
+ private fun showWatchedCountDialogWithList(totalEps: Int, watchedEps: Int?) {
+ val singleItems = (0..totalEps).map { it.toString() }.toTypedArray()
+ val checkedItem = watchedEps ?: 0
MaterialAlertDialogBuilder(requireContext())
- .setTitle("Background")
- .setMessage(
- if (background != null && background.isNotBlank())
- background
- else
- getString(R.string.n_a)
- ).setPositiveButton(android.R.string.ok) { dialog, _ ->
+ .setTitle(R.string.anime_watched_till_hint)
+ .setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
+ animeDetailsViewModel.setEpisodeCount(which)
dialog.dismiss()
- }.show()
+ }
+ .show()
}
- private fun openStatsDialog(statistics: AnimeByIDResponse.Statistics?) {
+ @SuppressLint("InflateParams")
+ private fun showWatchedCountDialogWithTextField(watchedEps: Int?) {
+ val view = LayoutInflater
+ .from(context)
+ .inflate(R.layout.custom_episode_count_dialog, null)
+ val binding: CustomEpisodeCountDialogBinding =
+ CustomEpisodeCountDialogBinding.bind(view)
+ binding.etNum.setText(watchedEps?.toString() ?: String.emptyString)
MaterialAlertDialogBuilder(requireContext())
- .setTitle("Statistics")
- .setMessage(
- statistics?.getStats() ?: getString(R.string.n_a)
- ).setPositiveButton(android.R.string.ok) { dialog, _ ->
+ .setView(binding.root)
+ .setPositiveButton(R.string.ok) { dialog, _ ->
+ val count = binding.etNum.text?.toString()?.toInt() ?: 0
+ animeDetailsViewModel.setEpisodeCount(count)
dialog.dismiss()
- }.show()
- }
-
- private fun showAlternateTitlesDialog(alternativeTitles: AnimeByIDResponse.AlternativeTitles?) {
- MaterialAlertDialogBuilder(requireContext())
- .setTitle("Alternate titles")
- .setMessage(
- alternativeTitles?.getFormattedString() ?: getString(R.string.n_a)
- ).setPositiveButton(android.R.string.ok) { dialog, _ ->
+ }
+ .setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
- }.show()
+ }
+ .show()
}
+
+ private fun openBackgroundDialog(background: String?) =
+ requireContext().showNoActionOkDialog(R.string.background, background)
+
+ private fun openStatsDialog(statistics: AnimeByIDResponse.Statistics?) =
+ requireContext().showNoActionOkDialog(
+ R.string.statistics,
+ context?.getAnimeStats(statistics)
+ )
+
+ private fun showAlternateTitlesDialog(alternativeTitles: AnimeByIDResponse.AlternativeTitles?) =
+ requireContext().showNoActionOkDialog(
+ R.string.alternate_titles_hint,
+ context?.getFormattedAnimeTitlesString(alternativeTitles)
+ )
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/vm/AnimeDetailsViewModel.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/vm/AnimeDetailsViewModel.kt
index 994e1c7..fdfbd5c 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/vm/AnimeDetailsViewModel.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_details/vm/AnimeDetailsViewModel.kt
@@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sharkaboi.mediahub.data.api.enums.AnimeStatus
import com.sharkaboi.mediahub.data.api.enums.animeStatusFromString
+import com.sharkaboi.mediahub.data.wrappers.MHError
import com.sharkaboi.mediahub.modules.anime_details.repository.AnimeDetailsRepository
import com.sharkaboi.mediahub.modules.anime_details.util.AnimeDetailsUpdateClass
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -187,7 +188,7 @@ class AnimeDetailsViewModel
_animeDetailState.setFailure(result.error.errorMessage)
}
} ?: run {
- _animeDetailState.setFailure("No data to update")
+ _animeDetailState.setFailure(MHError.InvalidStateError.errorMessage)
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_ranking/adapters/AnimeRankingDetailedAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_ranking/adapters/AnimeRankingDetailedAdapter.kt
index 3ceb94d..46fd7c8 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_ranking/adapters/AnimeRankingDetailedAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_ranking/adapters/AnimeRankingDetailedAdapter.kt
@@ -6,9 +6,9 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
import com.sharkaboi.mediahub.R
-import com.sharkaboi.mediahub.common.extensions.roundOfString
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.extensions.getRatingStringWithRating
import com.sharkaboi.mediahub.data.api.models.anime.AnimeRankingResponse
import com.sharkaboi.mediahub.databinding.AnimeListItemBinding
@@ -23,16 +23,16 @@ class AnimeRankingDetailedAdapter(
fun bind(item: AnimeRankingResponse.Data?) {
item?.let {
animeListItemBinding.apply {
- ivAnimeBanner.load(it.node.mainPicture?.large ?: it.node.mainPicture?.medium) {
- crossfade(true)
- placeholder(R.drawable.ic_anime_placeholder)
- error(R.drawable.ic_anime_placeholder)
- fallback(R.drawable.ic_anime_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ ivAnimeBanner.load(
+ uri = it.node.mainPicture?.large ?: it.node.mainPicture?.medium,
+ builder = UIConstants.AnimeImageBuilder
+ )
tvAnimeName.text = it.node.title
- tvEpisodesWatched.text = ("Rank : ${it.ranking.rank}")
- tvScore.text = ("★ ${it.node.meanScore?.roundOfString() ?: "0"}")
+ tvEpisodesWatched.text = tvEpisodesWatched.context.getString(
+ R.string.media_rank_template,
+ it.ranking.rank
+ )
+ tvScore.text = tvScore.context.getRatingStringWithRating(it.node.meanScore)
root.setOnClickListener {
onItemClick(item.node.id)
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_ranking/repository/AnimeRankingRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_ranking/repository/AnimeRankingRepositoryImpl.kt
index 78964f5..3ffc5bd 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_ranking/repository/AnimeRankingRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_ranking/repository/AnimeRankingRepositoryImpl.kt
@@ -4,7 +4,7 @@ import android.content.SharedPreferences
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.AnimeRankingType
import com.sharkaboi.mediahub.data.api.models.anime.AnimeRankingResponse
import com.sharkaboi.mediahub.data.api.retrofit.AnimeService
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_ranking/ui/AnimeRankingFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_ranking/ui/AnimeRankingFragment.kt
index 6ad3eaf..e95a66c 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_ranking/ui/AnimeRankingFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_ranking/ui/AnimeRankingFragment.kt
@@ -14,7 +14,7 @@ import androidx.paging.LoadState
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.chip.Chip
-import com.google.android.material.shape.ShapeAppearanceModel
+import com.sharkaboi.mediahub.common.constants.UIConstants.setMediaHubChipStyle
import com.sharkaboi.mediahub.common.extensions.showToast
import com.sharkaboi.mediahub.data.api.enums.AnimeRankingType
import com.sharkaboi.mediahub.databinding.FragmentAnimeRankingBinding
@@ -60,6 +60,7 @@ class AnimeRankingFragment : Fragment() {
setupFilterChips()
setUpRecyclerView()
setObservers()
+ collectPagedList()
}
private fun initRanking() {
@@ -82,14 +83,14 @@ class AnimeRankingFragment : Fragment() {
AnimeRankingType.values().forEach { rankingType ->
binding.rankTypeChipGroup.addView(
Chip(context).apply {
- text = rankingType.getAnimeRanking()
- setEnsureMinTouchTargetSize(false)
+ text = rankingType.getAnimeRanking(context)
+ setMediaHubChipStyle()
isCheckable = true
isChecked = rankingType == animeRankingViewModel.selectedRankingType
- shapeAppearanceModel = ShapeAppearanceModel().withCornerSize(8f)
setOnClickListener {
animeRankingViewModel.setRankingType(rankingType)
collectPagedList()
+ binding.rvAnimeRanking.smoothScrollToPosition(0)
}
}
)
@@ -99,7 +100,7 @@ class AnimeRankingFragment : Fragment() {
private fun setUpRecyclerView() {
binding.rvAnimeRanking.apply {
animeRankingDetailedAdapter = AnimeRankingDetailedAdapter { animeId ->
- val action = AnimeRankingFragmentDirections.openAnimeDetailsWithId(animeId)
+ val action = AnimeRankingFragmentDirections.openAnimeById(animeId)
navController.navigate(action)
}
layoutManager = GridLayoutManager(context, 3)
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_search/adapters/AnimeSearchListAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_search/adapters/AnimeSearchListAdapter.kt
index 1a3afed..51f3915 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_search/adapters/AnimeSearchListAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_search/adapters/AnimeSearchListAdapter.kt
@@ -7,9 +7,8 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
-import com.sharkaboi.mediahub.common.extensions.roundOfString
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.extensions.getRatingStringWithRating
import com.sharkaboi.mediahub.data.api.models.anime.AnimeSearchResponse
import com.sharkaboi.mediahub.databinding.AnimeListItemBinding
@@ -24,16 +23,13 @@ class AnimeSearchListAdapter(
fun bind(item: AnimeSearchResponse.Data?) {
item?.let {
animeListItemBinding.apply {
- ivAnimeBanner.load(it.node.mainPicture?.large ?: it.node.mainPicture?.medium) {
- crossfade(true)
- placeholder(R.drawable.ic_anime_placeholder)
- error(R.drawable.ic_anime_placeholder)
- fallback(R.drawable.ic_anime_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ ivAnimeBanner.load(
+ uri = it.node.mainPicture?.large ?: it.node.mainPicture?.medium,
+ builder = UIConstants.AnimeImageBuilder
+ )
tvAnimeName.text = it.node.title
tvEpisodesWatched.isVisible = false
- tvScore.text = ("★ ${it.node.meanScore?.roundOfString() ?: "0"}")
+ tvScore.text = tvScore.context.getRatingStringWithRating(it.node.meanScore)
root.setOnClickListener {
onItemClick(item.node.id)
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_search/repository/AnimeSearchRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_search/repository/AnimeSearchRepositoryImpl.kt
index 2f19c36..f39384f 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_search/repository/AnimeSearchRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_search/repository/AnimeSearchRepositoryImpl.kt
@@ -4,7 +4,7 @@ import android.content.SharedPreferences
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.models.anime.AnimeSearchResponse
import com.sharkaboi.mediahub.data.api.retrofit.AnimeService
import com.sharkaboi.mediahub.data.datastore.DataStoreRepository
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_search/ui/AnimeSearchFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_search/ui/AnimeSearchFragment.kt
index a147649..235b253 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_search/ui/AnimeSearchFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_search/ui/AnimeSearchFragment.kt
@@ -16,6 +16,8 @@ import androidx.paging.LoadState
import androidx.paging.PagingData
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager
+import com.sharkaboi.mediahub.BottomNavGraphDirections
+import com.sharkaboi.mediahub.R
import com.sharkaboi.mediahub.common.extensions.debounce
import com.sharkaboi.mediahub.common.extensions.showToast
import com.sharkaboi.mediahub.databinding.FragmentAnimeSearchBinding
@@ -62,7 +64,7 @@ class AnimeSearchFragment : Fragment() {
private fun setUpRecyclerView() {
binding.rvSearchResults.apply {
animeSearchListAdapter = AnimeSearchListAdapter { animeId ->
- val action = AnimeSearchFragmentDirections.openAnimeDetailsWithId(animeId)
+ val action = BottomNavGraphDirections.openAnimeById(animeId)
navController.navigate(action)
}
layoutManager = GridLayoutManager(context, 3)
@@ -82,7 +84,8 @@ class AnimeSearchFragment : Fragment() {
binding.progress.isShowing = loadStates.refresh is LoadState.Loading
binding.searchEmptyView.root.isVisible =
loadStates.refresh is LoadState.NotLoading && animeSearchListAdapter.itemCount == 0
- binding.searchEmptyView.tvHint.text = ("No anime found for query")
+ binding.searchEmptyView.tvHint.text =
+ getString(R.string.anime_search_no_result_hint)
}
}
val debounce = debounce(scope = lifecycleScope) {
@@ -99,7 +102,7 @@ class AnimeSearchFragment : Fragment() {
query?.toString()?.let {
if (it.length < 3) {
binding.searchEmptyView.root.isVisible = true
- binding.searchEmptyView.tvHint.text = ("Search for any anime")
+ binding.searchEmptyView.tvHint.text = getString(R.string.anime_search_hint)
animeSearchListAdapter.submitData(PagingData.empty())
return@launch
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_seasonal/adapters/AnimeSeasonalAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_seasonal/adapters/AnimeSeasonalAdapter.kt
index 4e2bfc9..5bec8f7 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_seasonal/adapters/AnimeSeasonalAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_seasonal/adapters/AnimeSeasonalAdapter.kt
@@ -7,9 +7,8 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
-import com.sharkaboi.mediahub.common.extensions.roundOfString
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.extensions.getRatingStringWithRating
import com.sharkaboi.mediahub.data.api.models.anime.AnimeSeasonalResponse
import com.sharkaboi.mediahub.databinding.AnimeListItemBinding
@@ -24,16 +23,13 @@ class AnimeSeasonalAdapter(
fun bind(item: AnimeSeasonalResponse.Data?) {
item?.let {
animeListItemBinding.apply {
- ivAnimeBanner.load(it.node.mainPicture?.large ?: it.node.mainPicture?.medium) {
- crossfade(true)
- placeholder(R.drawable.ic_anime_placeholder)
- error(R.drawable.ic_anime_placeholder)
- fallback(R.drawable.ic_anime_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ ivAnimeBanner.load(
+ uri = it.node.mainPicture?.large ?: it.node.mainPicture?.medium,
+ builder = UIConstants.AnimeImageBuilder
+ )
tvAnimeName.text = it.node.title
tvEpisodesWatched.isVisible = false
- tvScore.text = ("★ ${it.node.meanScore?.roundOfString() ?: "0"}")
+ tvScore.text = tvScore.context.getRatingStringWithRating(it.node.meanScore)
root.setOnClickListener {
onItemClick(item.node.id)
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_seasonal/repository/AnimeSeasonalRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_seasonal/repository/AnimeSeasonalRepositoryImpl.kt
index d8d6e91..207eee0 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_seasonal/repository/AnimeSeasonalRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_seasonal/repository/AnimeSeasonalRepositoryImpl.kt
@@ -4,7 +4,7 @@ import android.content.SharedPreferences
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.AnimeSeason
import com.sharkaboi.mediahub.data.api.models.anime.AnimeSeasonalResponse
import com.sharkaboi.mediahub.data.api.retrofit.AnimeService
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_seasonal/ui/AnimeSeasonalFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_seasonal/ui/AnimeSeasonalFragment.kt
index 7121da7..44024ad 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_seasonal/ui/AnimeSeasonalFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_seasonal/ui/AnimeSeasonalFragment.kt
@@ -13,6 +13,7 @@ import androidx.navigation.fragment.navArgs
import androidx.paging.LoadState
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager
+import com.sharkaboi.mediahub.BottomNavGraphDirections
import com.sharkaboi.mediahub.common.extensions.capitalizeFirst
import com.sharkaboi.mediahub.common.extensions.showToast
import com.sharkaboi.mediahub.data.api.enums.getAnimeSeason
@@ -60,6 +61,7 @@ class AnimeSeasonalFragment : Fragment() {
setupSeasonButtons()
setUpRecyclerView()
setObservers()
+ collectPagedList()
}
private fun initSeason() {
@@ -102,7 +104,7 @@ class AnimeSeasonalFragment : Fragment() {
private fun setUpRecyclerView() {
binding.rvAnimeSeasonal.apply {
animeSeasonalAdapter = AnimeSeasonalAdapter { animeId ->
- val action = AnimeSeasonalFragmentDirections.openAnimeDetailsWithId(animeId)
+ val action = BottomNavGraphDirections.openAnimeById(animeId)
navController.navigate(action)
}
layoutManager = GridLayoutManager(context, 3)
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_suggestions/adapters/AnimeSuggestionsAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_suggestions/adapters/AnimeSuggestionsAdapter.kt
index c6678dd..1ea72af 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_suggestions/adapters/AnimeSuggestionsAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_suggestions/adapters/AnimeSuggestionsAdapter.kt
@@ -7,9 +7,8 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
-import com.sharkaboi.mediahub.common.extensions.roundOfString
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.extensions.getRatingStringWithRating
import com.sharkaboi.mediahub.data.api.models.anime.AnimeSuggestionsResponse
import com.sharkaboi.mediahub.databinding.AnimeListItemBinding
@@ -24,16 +23,13 @@ class AnimeSuggestionsAdapter(
fun bind(item: AnimeSuggestionsResponse.Data?) {
item?.let {
animeListItemBinding.apply {
- ivAnimeBanner.load(it.node.mainPicture?.large ?: it.node.mainPicture?.medium) {
- crossfade(true)
- placeholder(R.drawable.ic_anime_placeholder)
- error(R.drawable.ic_anime_placeholder)
- fallback(R.drawable.ic_anime_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ ivAnimeBanner.load(
+ uri = it.node.mainPicture?.large ?: it.node.mainPicture?.medium,
+ builder = UIConstants.AnimeImageBuilder
+ )
tvAnimeName.text = it.node.title
tvEpisodesWatched.isVisible = false
- tvScore.text = ("★ ${it.node.meanScore?.roundOfString() ?: "0"}")
+ tvScore.text = tvScore.context.getRatingStringWithRating(it.node.meanScore)
root.setOnClickListener {
onItemClick(item.node.id)
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_suggestions/repository/AnimeSuggestionsRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_suggestions/repository/AnimeSuggestionsRepositoryImpl.kt
index 078eda3..20240d1 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_suggestions/repository/AnimeSuggestionsRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_suggestions/repository/AnimeSuggestionsRepositoryImpl.kt
@@ -4,7 +4,7 @@ import android.content.SharedPreferences
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.models.anime.AnimeSuggestionsResponse
import com.sharkaboi.mediahub.data.api.retrofit.AnimeService
import com.sharkaboi.mediahub.data.datastore.DataStoreRepository
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_suggestions/ui/AnimeSuggestionsFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_suggestions/ui/AnimeSuggestionsFragment.kt
index 0779bfb..2721184 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/anime_suggestions/ui/AnimeSuggestionsFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/anime_suggestions/ui/AnimeSuggestionsFragment.kt
@@ -12,6 +12,7 @@ import androidx.navigation.fragment.findNavController
import androidx.paging.LoadState
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager
+import com.sharkaboi.mediahub.BottomNavGraphDirections
import com.sharkaboi.mediahub.common.extensions.showToast
import com.sharkaboi.mediahub.databinding.FragmentAnimeSuggestionsBinding
import com.sharkaboi.mediahub.modules.anime_suggestions.adapters.AnimeSuggestionsAdapter
@@ -54,7 +55,7 @@ class AnimeSuggestionsFragment : Fragment() {
private fun setUpRecyclerView() {
binding.rvAnimeSuggestions.apply {
animeSuggestionsAdapter = AnimeSuggestionsAdapter { animeId ->
- val action = AnimeSuggestionsFragmentDirections.openAnimeById(animeId)
+ val action = BottomNavGraphDirections.openAnimeById(animeId)
navController.navigate(action)
}
layoutManager = GridLayoutManager(context, 3)
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/auth/repository/OAuthRepository.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/auth/repository/OAuthRepository.kt
index c6036b0..967faeb 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/auth/repository/OAuthRepository.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/auth/repository/OAuthRepository.kt
@@ -1,5 +1,7 @@
package com.sharkaboi.mediahub.modules.auth.repository
+import com.sharkaboi.mediahub.data.wrappers.MHTaskState
+
interface OAuthRepository {
- suspend fun getAccessToken(code: String, codeVerifier: String): String?
+ suspend fun getAccessToken(code: String, codeVerifier: String): MHTaskState
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/auth/repository/OAuthRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/auth/repository/OAuthRepositoryImpl.kt
index 13d1fb4..021ae09 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/auth/repository/OAuthRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/auth/repository/OAuthRepositoryImpl.kt
@@ -5,6 +5,8 @@ import com.sharkaboi.mediahub.BuildConfig
import com.sharkaboi.mediahub.common.extensions.emptyString
import com.sharkaboi.mediahub.data.api.retrofit.AuthService
import com.sharkaboi.mediahub.data.datastore.DataStoreRepository
+import com.sharkaboi.mediahub.data.wrappers.MHError
+import com.sharkaboi.mediahub.data.wrappers.MHTaskState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
@@ -14,7 +16,7 @@ class OAuthRepositoryImpl(
private val dataStoreRepository: DataStoreRepository
) : OAuthRepository {
- override suspend fun getAccessToken(code: String, codeVerifier: String): String? =
+ override suspend fun getAccessToken(code: String, codeVerifier: String): MHTaskState =
withContext(Dispatchers.IO) {
val response = authService.getAccessTokenAsync(
clientId = BuildConfig.clientId,
@@ -27,20 +29,38 @@ class OAuthRepositoryImpl(
dataStoreRepository.setAccessToken(response.body.accessToken)
dataStoreRepository.setExpireIn()
dataStoreRepository.setRefreshToken(response.body.refreshToken)
- return@withContext null
+ return@withContext MHTaskState(
+ isSuccess = true,
+ data = null,
+ error = MHError.EmptyError
+ )
}
is NetworkResponse.ServerError -> {
Timber.d(response.body.toString())
- return@withContext response.body?.message
- ?: "Error with status code : ${response.code}"
+ return@withContext MHTaskState(
+ isSuccess = false,
+ data = null,
+ error = response.body?.message?.let { MHError(it) }
+ ?: MHError.apiErrorWithCode(response.code)
+ )
}
is NetworkResponse.NetworkError -> {
Timber.d(response.error.message ?: String.emptyString)
- return@withContext response.error.message ?: "Error with network"
+ return@withContext MHTaskState(
+ isSuccess = false,
+ data = null,
+ error = response.error.message?.let { MHError(it) }
+ ?: MHError.NetworkError
+ )
}
is NetworkResponse.UnknownError -> {
Timber.d(response.error.message ?: String.emptyString)
- return@withContext response.error.message ?: "Error with parsing"
+ return@withContext MHTaskState(
+ isSuccess = false,
+ data = null,
+ error = response.error.message?.let { MHError(it) }
+ ?: MHError.ParsingError
+ )
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/auth/ui/OAuthActivity.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/auth/ui/OAuthActivity.kt
index 9c1f9d0..356b795 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/auth/ui/OAuthActivity.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/auth/ui/OAuthActivity.kt
@@ -94,10 +94,14 @@ class OAuthActivity : AppCompatActivity() {
val uri = intent?.data
Timber.d("onNewIntent uri :$uri")
if (uri != null && uri.toString().startsWith(AppConstants.oAuthDeepLinkUri)) {
- val code = uri.getQueryParameter("code")
+ val code = uri.getQueryParameter(CODE_QUERY_PARAM_KEY)
if (code != null) {
oAuthViewModel.receivedAuthToken(code)
}
}
}
+
+ companion object {
+ private const val CODE_QUERY_PARAM_KEY = "code"
+ }
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/auth/vm/OAuthViewModel.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/auth/vm/OAuthViewModel.kt
index de512cf..c01f3d9 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/auth/vm/OAuthViewModel.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/auth/vm/OAuthViewModel.kt
@@ -50,10 +50,10 @@ class OAuthViewModel
Timber.d("challenge retrieved: $codeChallenge")
Timber.d("code received: $code")
val result = oAuthRepository.getAccessToken(code, codeChallenge)
- if (result == null) {
+ if (result.isSuccess) {
_oAuthState.setOAuthSuccess()
} else {
- _oAuthState.setOAuthFailure(result)
+ _oAuthState.setOAuthFailure(result.error.errorMessage)
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/discover/adapters/AiringAnimeAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/discover/adapters/AiringAnimeAdapter.kt
index 00e23bf..36f8e18 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/discover/adapters/AiringAnimeAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/discover/adapters/AiringAnimeAdapter.kt
@@ -7,9 +7,8 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
-import com.sharkaboi.mediahub.common.extensions.roundOfString
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.extensions.getRatingStringWithRating
import com.sharkaboi.mediahub.data.api.models.anime.AnimeSeasonalResponse
import com.sharkaboi.mediahub.databinding.AnimeListItemBinding
@@ -63,16 +62,11 @@ class AiringAnimeAdapter(private val onClick: (Int) -> Unit) :
}
binding.tvAnimeName.text = item.node.title
binding.tvEpisodesWatched.isVisible = false
- binding.tvScore.text = ("★ ${item.node.meanScore?.roundOfString() ?: "0"}")
+ binding.tvScore.text = binding.tvScore.context.getRatingStringWithRating(item.node.meanScore)
binding.ivAnimeBanner.load(
- item.node.mainPicture?.large ?: item.node.mainPicture?.medium
- ) {
- crossfade(true)
- placeholder(R.drawable.ic_anime_placeholder)
- error(R.drawable.ic_anime_placeholder)
- fallback(R.drawable.ic_anime_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ uri = item.node.mainPicture?.large ?: item.node.mainPicture?.medium,
+ builder = UIConstants.AnimeImageBuilder
+ )
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/discover/adapters/AnimeRankingAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/discover/adapters/AnimeRankingAdapter.kt
index a9f6c5c..c50ecba 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/discover/adapters/AnimeRankingAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/discover/adapters/AnimeRankingAdapter.kt
@@ -7,9 +7,8 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
-import com.sharkaboi.mediahub.common.extensions.roundOfString
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.extensions.getRatingStringWithRating
import com.sharkaboi.mediahub.data.api.models.anime.AnimeRankingResponse
import com.sharkaboi.mediahub.databinding.AnimeListItemBinding
@@ -63,16 +62,11 @@ class AnimeRankingAdapter(private val onClick: (Int) -> Unit) :
}
binding.tvAnimeName.text = item.node.title
binding.tvEpisodesWatched.isVisible = false
- binding.tvScore.text = ("★ ${item.node.meanScore?.roundOfString() ?: "0"}")
+ binding.tvScore.text = binding.tvScore.context.getRatingStringWithRating(item.node.meanScore)
binding.ivAnimeBanner.load(
- item.node.mainPicture?.large ?: item.node.mainPicture?.medium
- ) {
- crossfade(true)
- placeholder(R.drawable.ic_anime_placeholder)
- error(R.drawable.ic_anime_placeholder)
- fallback(R.drawable.ic_anime_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ uri = item.node.mainPicture?.large ?: item.node.mainPicture?.medium,
+ builder = UIConstants.AnimeImageBuilder
+ )
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/discover/adapters/AnimeSuggestionsAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/discover/adapters/AnimeSuggestionsAdapter.kt
index 4133a6b..6f23b0c 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/discover/adapters/AnimeSuggestionsAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/discover/adapters/AnimeSuggestionsAdapter.kt
@@ -7,9 +7,8 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
-import com.sharkaboi.mediahub.common.extensions.roundOfString
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.extensions.getRatingStringWithRating
import com.sharkaboi.mediahub.data.api.models.anime.AnimeSuggestionsResponse
import com.sharkaboi.mediahub.databinding.AnimeListItemBinding
@@ -63,16 +62,11 @@ class AnimeSuggestionsAdapter(private val onClick: (Int) -> Unit) :
}
binding.tvAnimeName.text = item.node.title
binding.tvEpisodesWatched.isVisible = false
- binding.tvScore.text = ("★ ${item.node.meanScore?.roundOfString() ?: "0"}")
+ binding.tvScore.text = binding.tvScore.context.getRatingStringWithRating(item.node.meanScore)
binding.ivAnimeBanner.load(
- item.node.mainPicture?.large ?: item.node.mainPicture?.medium
- ) {
- crossfade(true)
- placeholder(R.drawable.ic_anime_placeholder)
- error(R.drawable.ic_anime_placeholder)
- fallback(R.drawable.ic_anime_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ uri = item.node.mainPicture?.large ?: item.node.mainPicture?.medium,
+ builder = UIConstants.AnimeImageBuilder
+ )
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/discover/repository/DiscoverRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/discover/repository/DiscoverRepositoryImpl.kt
index 8569204..874fe0e 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/discover/repository/DiscoverRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/discover/repository/DiscoverRepositoryImpl.kt
@@ -3,8 +3,7 @@ package com.sharkaboi.mediahub.modules.discover.repository
import android.content.SharedPreferences
import com.haroldadmin.cnradapter.NetworkResponse
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.common.extensions.ifNullOrBlank
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.getAnimeSeason
import com.sharkaboi.mediahub.data.api.models.anime.AnimeRankingResponse
import com.sharkaboi.mediahub.data.api.models.anime.AnimeSeasonalResponse
@@ -36,7 +35,7 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError("Log in has expired, Log in again.", null)
+ error = MHError.LoginExpiredError
)
} else {
val result = animeService.getAnimeSuggestionsAsync(
@@ -59,10 +58,8 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with network" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.NetworkError
)
}
is NetworkResponse.ServerError -> {
@@ -70,10 +67,8 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.body?.message.ifNullOrBlank { "Error with status code : ${result.code}" },
- null
- )
+ error = result.body?.message?.let { MHError(it) }
+ ?: MHError.apiErrorWithCode(result.code)
)
}
is NetworkResponse.UnknownError -> {
@@ -81,10 +76,8 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with parsing" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.ParsingError
)
}
}
@@ -95,7 +88,7 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(e.message.ifNullOrBlank { "Unknown Error" }, e)
+ error = e.message?.let { MHError(it) } ?: MHError.UnknownError
)
}
}
@@ -110,7 +103,7 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError("Log in has expired, Log in again.", null)
+ error = MHError.LoginExpiredError
)
} else {
val today = LocalDate.now()
@@ -136,10 +129,8 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with network" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.NetworkError
)
}
is NetworkResponse.ServerError -> {
@@ -147,10 +138,8 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.body?.message.ifNullOrBlank { "Error with status code : ${result.code}" },
- null
- )
+ error = result.body?.message?.let { MHError(it) }
+ ?: MHError.apiErrorWithCode(result.code)
)
}
is NetworkResponse.UnknownError -> {
@@ -158,10 +147,8 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with parsing" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.ParsingError
)
}
}
@@ -172,7 +159,7 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(e.message.ifNullOrBlank { "Unknown Error" }, e)
+ error = e.message?.let { MHError(it) } ?: MHError.UnknownError
)
}
}
@@ -187,7 +174,7 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError("Log in has expired, Log in again.", null)
+ error = MHError.LoginExpiredError
)
} else {
val result = animeService.getAnimeRankingAsync(
@@ -210,10 +197,8 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with network" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.NetworkError
)
}
is NetworkResponse.ServerError -> {
@@ -221,10 +206,8 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.body?.message.ifNullOrBlank { "Error with status code : ${result.code}" },
- null
- )
+ error = result.body?.message?.let { MHError(it) }
+ ?: MHError.apiErrorWithCode(result.code)
)
}
is NetworkResponse.UnknownError -> {
@@ -232,10 +215,8 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with parsing" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.ParsingError
)
}
}
@@ -246,7 +227,7 @@ class DiscoverRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(e.message.ifNullOrBlank { "Unknown Error" }, e)
+ error = e.message?.let { MHError(it) } ?: MHError.UnknownError
)
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/discover/ui/DiscoverFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/discover/ui/DiscoverFragment.kt
index 1a1b6e6..40313c2 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/discover/ui/DiscoverFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/discover/ui/DiscoverFragment.kt
@@ -10,9 +10,13 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
+import com.sharkaboi.mediahub.BottomNavGraphDirections
import com.sharkaboi.mediahub.common.extensions.addFooter
import com.sharkaboi.mediahub.common.extensions.observe
import com.sharkaboi.mediahub.common.extensions.showToast
+import com.sharkaboi.mediahub.data.api.models.anime.AnimeRankingResponse
+import com.sharkaboi.mediahub.data.api.models.anime.AnimeSeasonalResponse
+import com.sharkaboi.mediahub.data.api.models.anime.AnimeSuggestionsResponse
import com.sharkaboi.mediahub.databinding.FragmentDiscoverBinding
import com.sharkaboi.mediahub.modules.discover.adapters.AiringAnimeAdapter
import com.sharkaboi.mediahub.modules.discover.adapters.AnimeRankingAdapter
@@ -86,59 +90,64 @@ class DiscoverFragment : Fragment() {
}
private fun setupRecyclerViews(discoverAnimeListWrapper: DiscoverAnimeListWrapper) {
- binding.tvAnimeRecommendationsEmpty.isVisible =
- discoverAnimeListWrapper.animeSuggestions.isEmpty()
- binding.rvAnimeRecommendations.apply {
- adapter = AnimeSuggestionsAdapter { animeId ->
- val action = DiscoverFragmentDirections.openAnimeById(animeId)
- navController.navigate(action)
- }.apply {
- submitList(discoverAnimeListWrapper.animeSuggestions)
- }.addFooter {
- LoadMoreAdapter {
- navController.navigate(DiscoverFragmentDirections.openAnimeSuggestions())
- }
+ setupAnimeRecommendationsList(discoverAnimeListWrapper.animeSuggestions)
+ setupAnimeAiringList(discoverAnimeListWrapper.animeOfCurrentSeason)
+ setupAnimeRankingList(discoverAnimeListWrapper.animeRankings)
+ }
+
+ private fun setupAnimeRankingList(animeRankings: List) {
+ binding.tvAnimeRankingEmpty.isVisible = animeRankings.isEmpty()
+ binding.rvAnimeRanking.adapter = AnimeRankingAdapter { animeId ->
+ openAnimeWithId(animeId)
+ }.apply {
+ submitList(animeRankings)
+ }.addFooter {
+ LoadMoreAdapter {
+ navController.navigate(DiscoverFragmentDirections.openAnimeRankings(null))
}
- layoutManager =
- LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- setHasFixedSize(true)
- itemAnimator = DefaultItemAnimator()
}
- binding.tvAnimeAiringEmpty.isVisible =
- discoverAnimeListWrapper.animeOfCurrentSeason.isEmpty()
- binding.rvAnimeAiring.apply {
- adapter = AiringAnimeAdapter { animeId ->
- val action = DiscoverFragmentDirections.openAnimeById(animeId)
- navController.navigate(action)
- }.apply {
- submitList(discoverAnimeListWrapper.animeOfCurrentSeason)
- }.addFooter {
- LoadMoreAdapter {
- navController.navigate(DiscoverFragmentDirections.openAnimeSeasonals(null, 0))
- }
+ binding.rvAnimeRanking.layoutManager =
+ LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ binding.rvAnimeRanking.setHasFixedSize(true)
+ binding.rvAnimeRanking.itemAnimator = DefaultItemAnimator()
+ }
+
+ private fun setupAnimeAiringList(animeOfCurrentSeason: List) {
+ binding.tvAnimeAiringEmpty.isVisible = animeOfCurrentSeason.isEmpty()
+ binding.rvAnimeAiring.adapter = AiringAnimeAdapter { animeId ->
+ openAnimeWithId(animeId)
+ }.apply {
+ submitList(animeOfCurrentSeason)
+ }.addFooter {
+ LoadMoreAdapter {
+ navController.navigate(DiscoverFragmentDirections.openAnimeSeasonals(null, 0))
}
- layoutManager =
- LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- setHasFixedSize(true)
- itemAnimator = DefaultItemAnimator()
}
- binding.tvAnimeRankingEmpty.isVisible =
- discoverAnimeListWrapper.animeRankings.isEmpty()
- binding.rvAnimeRanking.apply {
- adapter = AnimeRankingAdapter { animeId ->
- val action = DiscoverFragmentDirections.openAnimeById(animeId)
- navController.navigate(action)
- }.apply {
- submitList(discoverAnimeListWrapper.animeRankings)
- }.addFooter {
- LoadMoreAdapter {
- navController.navigate(DiscoverFragmentDirections.openAnimeRankings(null))
- }
+ binding.rvAnimeAiring.layoutManager =
+ LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ binding.rvAnimeAiring.setHasFixedSize(true)
+ binding.rvAnimeAiring.itemAnimator = DefaultItemAnimator()
+ }
+
+ private fun setupAnimeRecommendationsList(animeSuggestions: List) {
+ binding.tvAnimeRecommendationsEmpty.isVisible = animeSuggestions.isEmpty()
+ binding.rvAnimeRecommendations.adapter = AnimeSuggestionsAdapter { animeId ->
+ openAnimeWithId(animeId)
+ }.apply {
+ submitList(animeSuggestions)
+ }.addFooter {
+ LoadMoreAdapter {
+ navController.navigate(DiscoverFragmentDirections.openAnimeSuggestions())
}
- layoutManager =
- LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- setHasFixedSize(true)
- itemAnimator = DefaultItemAnimator()
}
+ binding.rvAnimeRecommendations.layoutManager =
+ LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ binding.rvAnimeRecommendations.setHasFixedSize(true)
+ binding.rvAnimeRecommendations.itemAnimator = DefaultItemAnimator()
+ }
+
+ private fun openAnimeWithId(animeId: Int) {
+ val action = BottomNavGraphDirections.openAnimeById(animeId)
+ navController.navigate(action)
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/main/ui/MainActivity.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/main/ui/MainActivity.kt
index 8607e65..e4147c7 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/main/ui/MainActivity.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/main/ui/MainActivity.kt
@@ -14,6 +14,8 @@ import androidx.navigation.ui.setupWithNavController
import com.sharkaboi.mediahub.R
import com.sharkaboi.mediahub.common.extensions.observe
import com.sharkaboi.mediahub.common.extensions.startAnim
+import com.sharkaboi.mediahub.common.util.forceLaunchInBrowser
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.databinding.ActivityMainBinding
import com.sharkaboi.mediahub.modules.auth.ui.OAuthActivity
import com.sharkaboi.mediahub.modules.main.vm.MainViewModel
@@ -43,7 +45,20 @@ class MainActivity : AppCompatActivity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
- navController.handleDeepLink(intent)
+ if (intent?.dataString?.matches(ApiConstants.appAcceptedDeepLinkRegex) == true) {
+ val result = navController.handleDeepLink(intent)
+ if (result.not()) {
+ handleNonSupportedIntent(intent)
+ }
+ } else {
+ handleNonSupportedIntent(intent)
+ }
+ }
+
+ private fun handleNonSupportedIntent(intent: Intent?) {
+ val url = intent?.dataString
+ Timber.d("Can't handle intent $url")
+ url?.let { forceLaunchInBrowser(it) }
}
private fun configBottomNav() {
@@ -71,7 +86,7 @@ class MainActivity : AppCompatActivity() {
}
}
- fun setVisibilityAndListeners(@IdRes id: Int) {
+ private fun setVisibilityAndListeners(@IdRes id: Int) {
val isAnimeItem =
id == R.id.anime_item && navController.currentDestination?.id == R.id.anime_item
val isMangaItem =
@@ -82,11 +97,11 @@ class MainActivity : AppCompatActivity() {
binding.circleAnimeView.isVisible = true
binding.circleAnimeView.startAnim(anim) {
binding.fabSearch.isVisible = isAnimeItem || isMangaItem
- binding.circleAnimeView.isVisible = false
when {
isAnimeItem -> navController.navigate(R.id.openAnimeSearch)
isMangaItem -> navController.navigate(R.id.openMangaSearch)
}
+ binding.circleAnimeView.isVisible = false
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga/adapters/MangaListAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga/adapters/MangaListAdapter.kt
index 02a5436..737cce4 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga/adapters/MangaListAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga/adapters/MangaListAdapter.kt
@@ -6,8 +6,9 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.extensions.getProgressStringWith
+import com.sharkaboi.mediahub.common.extensions.getRatingStringWithRating
import com.sharkaboi.mediahub.data.api.models.usermanga.UserMangaListResponse
import com.sharkaboi.mediahub.databinding.MangaListItemBinding
@@ -22,23 +23,20 @@ class MangaListAdapter(
fun bind(item: UserMangaListResponse.Data?) {
item?.let {
mangaListItemBinding.apply {
- ivMangaBanner.load(it.node.mainPicture?.large ?: it.node.mainPicture?.medium) {
- crossfade(true)
- placeholder(R.drawable.ic_manga_placeholder)
- error(R.drawable.ic_manga_placeholder)
- fallback(R.drawable.ic_manga_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ ivMangaBanner.load(
+ uri = it.node.mainPicture?.large ?: it.node.mainPicture?.medium,
+ builder = UIConstants.MangaImageBuilder
+ )
tvMangaName.text = it.node.title
- val numChapters =
- if (it.node.numChapters == 0) "??" else it.node.numChapters
- tvChapsRead.text =
- ("${it.listStatus.numChaptersRead}/$numChapters")
- val numVolumes =
- if (it.node.numVolumes == 0) "??" else it.node.numVolumes
- tvVolumesRead.text =
- ("${it.listStatus.numVolumesRead}/$numVolumes")
- tvScore.text = ("★ ${it.listStatus.score}")
+ tvChapsRead.text = tvChapsRead.context.getProgressStringWith(
+ it.listStatus.numChaptersRead,
+ it.node.numChapters
+ )
+ tvVolumesRead.text = tvVolumesRead.context.getProgressStringWith(
+ it.listStatus.numVolumesRead,
+ it.node.numVolumes
+ )
+ tvScore.text = tvScore.context.getRatingStringWithRating(it.listStatus.score)
root.setOnClickListener {
onItemClick(item.node.id)
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga/repository/MangaRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga/repository/MangaRepositoryImpl.kt
index b340339..f7f6edf 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga/repository/MangaRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga/repository/MangaRepositoryImpl.kt
@@ -4,7 +4,7 @@ import android.content.SharedPreferences
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.MangaStatus
import com.sharkaboi.mediahub.data.api.enums.UserMangaSortType
import com.sharkaboi.mediahub.data.api.models.usermanga.UserMangaListResponse
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga/ui/MangaFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga/ui/MangaFragment.kt
index 5e98c6e..c737f25 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga/ui/MangaFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga/ui/MangaFragment.kt
@@ -47,7 +47,7 @@ class MangaFragment : Fragment() {
binding.vpManga.adapter = vpMangaAdapter
binding.mangaTabLayout.apply {
MangaStatus.values().forEach {
- addTab(newTab().apply { text = it.getFormattedString() })
+ addTab(newTab().apply { text = it.getFormattedString(context) })
}
}
onTabChanged = object : TabLayout.OnTabSelectedListener {
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga/ui/MangaListByStatusFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga/ui/MangaListByStatusFragment.kt
index f3a0743..2ef5507 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga/ui/MangaListByStatusFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga/ui/MangaListByStatusFragment.kt
@@ -13,6 +13,9 @@ import androidx.paging.LoadState
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.sharkaboi.mediahub.BottomNavGraphDirections
+import com.sharkaboi.mediahub.R
+import com.sharkaboi.mediahub.common.constants.UIConstants
import com.sharkaboi.mediahub.common.extensions.showToast
import com.sharkaboi.mediahub.data.api.enums.MangaStatus
import com.sharkaboi.mediahub.data.api.enums.UserMangaSortType
@@ -75,10 +78,10 @@ class MangaListByStatusFragment : Fragment() {
private fun setUpRecyclerView() {
binding.rvMangaByStatus.apply {
mangaListAdapter = MangaListAdapter { mangaId ->
- val action = MangaFragmentDirections.openMangaDetailsWithId(mangaId)
+ val action = BottomNavGraphDirections.openMangaById(mangaId)
navController.navigate(action)
}
- layoutManager = GridLayoutManager(context, 3)
+ layoutManager = GridLayoutManager(context, UIConstants.AnimeAndMangaGridSpanCount)
itemAnimator = DefaultItemAnimator()
adapter = mangaListAdapter.withLoadStateFooter(
footer = MangaLoadStateAdapter()
@@ -113,16 +116,15 @@ class MangaListByStatusFragment : Fragment() {
}
private fun openSortMenu() {
- val singleItems = UserMangaSortType.getFormattedArray()
+ val singleItems = UserMangaSortType.getFormattedArray(requireContext())
val checkedItem = UserMangaSortType.values().indexOf(mangaViewModel.currentChosenSortType)
MaterialAlertDialogBuilder(requireContext())
- .setTitle("Sort manga by")
+ .setTitle(R.string.sort_manga_by_hint)
.setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
mangaViewModel.setSortType(UserMangaSortType.values()[which])
getMangaList()
dialog.dismiss()
- }
- .show()
+ }.show()
}
private fun getMangaList() {
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga/vm/MangaViewModel.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga/vm/MangaViewModel.kt
index 6986009..f0b1ba4 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga/vm/MangaViewModel.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga/vm/MangaViewModel.kt
@@ -18,7 +18,7 @@ class MangaViewModel
private val mangaRepository: MangaRepository
) : ViewModel() {
private var _currentChosenMangaStatus: MangaStatus = MangaStatus.all
- val currentChosenMangaStatus get() = _currentChosenMangaStatus
+ private val currentChosenMangaStatus get() = _currentChosenMangaStatus
private var _currentChosenSortType: UserMangaSortType = UserMangaSortType.list_updated_at
val currentChosenSortType get() = _currentChosenSortType
private var _pagedMangaList: Flow>? = null
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/adapters/RecommendedMangaAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/adapters/RecommendedMangaAdapter.kt
index 876e8b9..92cb9b2 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/adapters/RecommendedMangaAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/adapters/RecommendedMangaAdapter.kt
@@ -8,8 +8,8 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
import com.sharkaboi.mediahub.R
+import com.sharkaboi.mediahub.common.constants.UIConstants
import com.sharkaboi.mediahub.data.api.models.manga.MangaByIDResponse
import com.sharkaboi.mediahub.databinding.MangaListItemBinding
@@ -64,17 +64,16 @@ class RecommendedMangaAdapter(private val onClick: (Int) -> Unit) :
binding.tvMangaName.text = item.node.title
binding.tvVolumesRead.isVisible = false
binding.tvChapsRead.text =
- ("Recommended ${item.numRecommendations} ${if (item.numRecommendations == 1) "time" else "times"}")
+ binding.tvChapsRead.context.resources.getQuantityString(
+ R.plurals.recommendation_times,
+ item.numRecommendations,
+ item.numRecommendations
+ )
binding.cardRating.isGone = true
binding.ivMangaBanner.load(
- item.node.mainPicture?.large ?: item.node.mainPicture?.medium
- ) {
- crossfade(true)
- placeholder(R.drawable.ic_manga_placeholder)
- error(R.drawable.ic_manga_placeholder)
- fallback(R.drawable.ic_manga_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ uri = item.node.mainPicture?.large ?: item.node.mainPicture?.medium,
+ builder = UIConstants.MangaImageBuilder
+ )
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/adapters/RelatedAnimeAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/adapters/RelatedAnimeAdapter.kt
index 5855f23..f936cda 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/adapters/RelatedAnimeAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/adapters/RelatedAnimeAdapter.kt
@@ -7,8 +7,7 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
+import com.sharkaboi.mediahub.common.constants.UIConstants
import com.sharkaboi.mediahub.data.api.models.manga.MangaByIDResponse
import com.sharkaboi.mediahub.databinding.AnimeListItemBinding
@@ -64,14 +63,9 @@ class RelatedAnimeAdapter(private val onClick: (Int) -> Unit) :
binding.tvEpisodesWatched.text = item.relationTypeFormatted
binding.cardRating.isGone = true
binding.ivAnimeBanner.load(
- item.node.mainPicture?.large ?: item.node.mainPicture?.medium
- ) {
- crossfade(true)
- placeholder(R.drawable.ic_anime_placeholder)
- error(R.drawable.ic_anime_placeholder)
- fallback(R.drawable.ic_anime_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ uri = item.node.mainPicture?.large ?: item.node.mainPicture?.medium,
+ builder = UIConstants.AnimeImageBuilder
+ )
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/adapters/RelatedMangaAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/adapters/RelatedMangaAdapter.kt
index f00a681..b784549 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/adapters/RelatedMangaAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/adapters/RelatedMangaAdapter.kt
@@ -8,8 +8,7 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
+import com.sharkaboi.mediahub.common.constants.UIConstants
import com.sharkaboi.mediahub.data.api.models.manga.MangaByIDResponse
import com.sharkaboi.mediahub.databinding.MangaListItemBinding
@@ -66,14 +65,9 @@ class RelatedMangaAdapter(private val onClick: (Int) -> Unit) :
binding.cardRating.isGone = true
binding.tvVolumesRead.isVisible = false
binding.ivMangaBanner.load(
- item.node.mainPicture?.large ?: item.node.mainPicture?.medium
- ) {
- crossfade(true)
- placeholder(R.drawable.ic_manga_placeholder)
- error(R.drawable.ic_manga_placeholder)
- fallback(R.drawable.ic_manga_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ uri = item.node.mainPicture?.large ?: item.node.mainPicture?.medium,
+ builder = UIConstants.MangaImageBuilder
+ )
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/repository/MangaDetailsRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/repository/MangaDetailsRepositoryImpl.kt
index 9bd30fa..78509eb 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/repository/MangaDetailsRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/repository/MangaDetailsRepositoryImpl.kt
@@ -2,8 +2,7 @@ package com.sharkaboi.mediahub.modules.manga_details.repository
import com.haroldadmin.cnradapter.NetworkResponse
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.common.extensions.ifNullOrBlank
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.models.manga.MangaByIDResponse
import com.sharkaboi.mediahub.data.api.retrofit.MangaService
import com.sharkaboi.mediahub.data.api.retrofit.UserMangaService
@@ -29,7 +28,7 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError("Log in has expired, Log in again.", null)
+ error = MHError.LoginExpiredError
)
} else {
val result = mangaService.getMangaByIdAsync(
@@ -50,10 +49,8 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with network" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.NetworkError
)
}
is NetworkResponse.ServerError -> {
@@ -61,10 +58,8 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.body?.message.ifNullOrBlank { "Error with status code : ${result.code}" },
- null
- )
+ error = result.body?.message?.let { MHError(it) }
+ ?: MHError.apiErrorWithCode(result.code)
)
}
is NetworkResponse.UnknownError -> {
@@ -72,10 +67,8 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with parsing" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.ParsingError
)
}
}
@@ -86,7 +79,7 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(e.message.ifNullOrBlank { "Unknown Error" }, e)
+ error = e.message?.let { MHError(it) } ?: MHError.UnknownError
)
}
}
@@ -101,7 +94,7 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError("Log in has expired, Log in again.", null)
+ error = MHError.LoginExpiredError
)
} else {
val result = userMangaService.updateMangaStatusAsync(
@@ -126,10 +119,8 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with network" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.NetworkError
)
}
is NetworkResponse.ServerError -> {
@@ -137,10 +128,8 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.body?.message.ifNullOrBlank { "Error with status code : ${result.code}" },
- null
- )
+ error = result.body?.message?.let { MHError(it) }
+ ?: MHError.apiErrorWithCode(result.code)
)
}
is NetworkResponse.UnknownError -> {
@@ -148,10 +137,8 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with parsing" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.ParsingError
)
}
}
@@ -162,7 +149,7 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(e.message.ifNullOrBlank { "Unknown Error" }, e)
+ error = e.message?.let { MHError(it) } ?: MHError.UnknownError
)
}
}
@@ -175,7 +162,7 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError("Log in has expired, Log in again.", null)
+ error = MHError.LoginExpiredError
)
} else {
val result = userMangaService.deleteMangaFromListAsync(
@@ -196,10 +183,8 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with network" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.NetworkError
)
}
is NetworkResponse.ServerError -> {
@@ -208,18 +193,14 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- "Manga isn't in your list", null
- )
+ error = MHError.MangaNotFoundError
)
}
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.body?.message.ifNullOrBlank { "Error with status code : ${result.code}" },
- null
- )
+ error = result.body?.message?.let { MHError(it) }
+ ?: MHError.apiErrorWithCode(result.code)
)
}
is NetworkResponse.UnknownError -> {
@@ -227,10 +208,8 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with parsing" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.ParsingError
)
}
}
@@ -241,7 +220,7 @@ class MangaDetailsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(e.message.ifNullOrBlank { "Unknown Error" }, e)
+ error = e.message?.let { MHError(it) } ?: MHError.UnknownError
)
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/ui/MangaDetailsFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/ui/MangaDetailsFragment.kt
index 10fde4e..ee83693 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/ui/MangaDetailsFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/ui/MangaDetailsFragment.kt
@@ -17,15 +17,18 @@ import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import coil.load
-import coil.transform.RoundedCornersTransformation
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.shape.ShapeAppearanceModel
+import com.sharkaboi.mediahub.BottomNavGraphDirections
import com.sharkaboi.mediahub.R
-import com.sharkaboi.mediahub.common.constants.MALExternalLinks
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.constants.UIConstants.setMediaHubChipStyle
import com.sharkaboi.mediahub.common.extensions.*
import com.sharkaboi.mediahub.common.util.openUrl
+import com.sharkaboi.mediahub.data.api.constants.MALExternalLinks
import com.sharkaboi.mediahub.data.api.enums.MangaStatus
+import com.sharkaboi.mediahub.data.api.enums.getMangaNsfwRating
+import com.sharkaboi.mediahub.data.api.enums.getMangaPublishStatus
import com.sharkaboi.mediahub.data.api.models.manga.MangaByIDResponse
import com.sharkaboi.mediahub.databinding.CustomReadCountDialogBinding
import com.sharkaboi.mediahub.databinding.FragmentMangaDetailsBinding
@@ -89,28 +92,17 @@ class MangaDetailsFragment : Fragment() {
else -> Unit
}
}
+
private val handleListStatusUpdate = { state: MangaDetailsUpdateClass ->
binding.mangaDetailsUserListCard.apply {
btnStatus.text =
- state.mangaStatus?.getFormattedString()
+ state.mangaStatus?.getFormattedString(requireContext())
?: getString(R.string.not_added)
- btnScore.text = ("${state.score ?: 0}/10")
- btnCountVolumes.text = (
- "${state.numReadVolumes ?: 0}/${
- if (state.totalVolumes == 0)
- "??"
- else
- state.totalVolumes.toString()
- }"
- )
- btnCountChaps.text = (
- "${state.numReadChapters ?: 0}/${
- if (state.totalChapters == 0)
- "??"
- else
- state.totalChapters.toString()
- }"
- )
+ btnScore.text = getString(R.string.media_rating_template, state.score ?: 0)
+ btnCountVolumes.text =
+ context?.getProgressStringWith(state.numReadVolumes, state.totalVolumes)
+ btnCountChaps.text =
+ context?.getProgressStringWith(state.numReadChapters, state.totalChapters)
btnScore.setOnClickListener {
openScoreDialog(state.score)
}
@@ -134,155 +126,74 @@ class MangaDetailsFragment : Fragment() {
}
private fun setData(mangaByIDResponse: MangaByIDResponse) {
- binding.apply {
- tvMangaName.text = mangaByIDResponse.title
- tvAlternateTitles.setOnClickListener {
- showAlternateTitlesDialog(mangaByIDResponse.alternativeTitles)
- }
- tvStartDate.text =
- mangaByIDResponse.startDate?.tryParseDate()?.formatDateDMY()
- ?: getString(R.string.n_a)
- tvEndDate.text =
- mangaByIDResponse.endDate?.tryParseDate()?.formatDateDMY()
- ?: getString(R.string.n_a)
- tvMeanScore.text = mangaByIDResponse.mean?.toString() ?: getString(R.string.n_a)
- tvRank.text = mangaByIDResponse.rank?.toString() ?: getString(R.string.n_a)
- tvPopularityRank.text = mangaByIDResponse.popularity.toString()
- authorsChipGroup.apply {
- removeAllViews()
- if (mangaByIDResponse.authors.isEmpty()) {
- addView(
- TextView(context).apply {
- text = getString(R.string.n_a)
- }
- )
- } else {
- mangaByIDResponse.authors.forEach { author ->
- addView(
- TextView(context).apply {
- setTextColor(ContextCompat.getColor(context, R.color.colorPrimary))
- setTypeface(null, Typeface.BOLD)
- text = ("${author.node.firstName} ${author.node.lastName} ")
- setOnClickListener {
- openUrl(
- MALExternalLinks.getMangaAuthorPageLink(author)
- )
- }
- }
- )
- }
- }
- }
- ivMangaMainPicture.load(
- mangaByIDResponse.mainPicture?.large ?: mangaByIDResponse.mainPicture?.medium
- ) {
- crossfade(true)
- placeholder(R.drawable.ic_manga_placeholder)
- error(R.drawable.ic_manga_placeholder)
- fallback(R.drawable.ic_manga_placeholder)
- transformations(RoundedCornersTransformation(8f))
- }
- ivMangaMainPicture.setOnClickListener {
- openImagesViewPager(mangaByIDResponse.pictures)
- }
- otherDetails.tvSynopsis.text =
- mangaByIDResponse.synopsis?.ifBlank { getString(R.string.n_a) }
- ?: getString(R.string.n_a)
- otherDetails.tvSynopsis.setOnClickListener {
- showFullSynopsisDialog(
- mangaByIDResponse.synopsis?.ifBlank { getString(R.string.n_a) }
- ?: getString(R.string.n_a)
- )
- }
- otherDetails.tvNsfwRating.text =
- mangaByIDResponse.nsfw?.getMangaNsfwRating() ?: getString(R.string.n_a)
- mangaByIDResponse.genres.let {
- otherDetails.genresChipGroup.removeAllViews()
- if (it.isEmpty()) {
- otherDetails.genresChipGroup.addView(
- Chip(context).apply {
- setEnsureMinTouchTargetSize(false)
- shapeAppearanceModel = ShapeAppearanceModel().withCornerSize(8f)
- text = getString(R.string.n_a)
- }
- )
- } else {
- it.forEach { genre ->
- otherDetails.genresChipGroup.addView(
- Chip(context).apply {
- setEnsureMinTouchTargetSize(false)
- shapeAppearanceModel = ShapeAppearanceModel().withCornerSize(8f)
- text = genre.name
- setOnClickListener {
- openUrl(
- MALExternalLinks.getMangaGenresLink(
- genre
- )
- )
- }
- }
- )
- }
- }
- }
- otherDetails.btnMediaType.text =
- ("Type : ${mangaByIDResponse.mediaType.replace('_', ' ').capitalizeFirst()}")
- otherDetails.btnMediaType.setOnClickListener {
- val action =
- MangaDetailsFragmentDirections.openMangaRankings(mangaByIDResponse.mediaType)
- navController.navigate(action)
- }
- otherDetails.btnMangaCurrentStatus.text =
- mangaByIDResponse.status.getMangaPublishStatus()
- otherDetails.btnTotalVols.text =
- mangaByIDResponse.numVolumes.let {
- if (it == 0)
- "${getString(R.string.n_a)} vols"
- else
- "$it ${if (it == 1) "vol" else "vols"}"
- }
- otherDetails.btnTotalChaps.text =
- mangaByIDResponse.numChapters.let {
- if (it == 0)
- "${getString(R.string.n_a)} chaps"
- else
- "$it ${if (it == 1) "chap" else "chaps"}"
- }
- otherDetails.serializationChipGroup.apply {
- removeAllViews()
- if (mangaByIDResponse.serialization.isEmpty()) {
- addView(
- TextView(context).apply {
- text = getString(R.string.n_a)
- }
- )
- } else {
- mangaByIDResponse.serialization.forEach { magazine ->
- addView(
- TextView(context).apply {
- setTextColor(ContextCompat.getColor(context, R.color.colorPrimary))
- setTypeface(null, Typeface.BOLD)
- text = ("${magazine.node.name} ")
- setOnClickListener {
- openUrl(
- MALExternalLinks.getMangaSerializationPageLink(magazine)
- )
- }
- }
- )
- }
- }
- }
- otherDetails.chipGroupOptions.forEach {
- if (it is Chip) {
- it.setEnsureMinTouchTargetSize(false)
- it.shapeAppearanceModel = ShapeAppearanceModel().withCornerSize(8f)
- }
- }
- otherDetails.btnBackground.setOnClickListener {
+ setupMangaMainDetailLayout(mangaByIDResponse)
+ setupMangaUserListStatusCard(mangaByIDResponse)
+ setupMangaOtherDetailLayout(mangaByIDResponse)
+ setupMangaOtherDetailsCard(mangaByIDResponse)
+ setupMangaOtherDetailsButtons(mangaByIDResponse)
+ setupMangaRecommendationsList(mangaByIDResponse.recommendations)
+ setupRelatedMangaList(mangaByIDResponse.relatedManga)
+ setupRelatedAnimeList(mangaByIDResponse.relatedAnime)
+ }
+
+ private fun setupRelatedMangaList(relatedManga: List) {
+ val rvRelatedManga = binding.otherDetails.rvRelatedManga
+ rvRelatedManga.adapter = RelatedMangaAdapter { mangaId ->
+ openMangaWithId(mangaId)
+ }.apply {
+ submitList(relatedManga)
+ }
+ binding.otherDetails.tvRelatedMangaHint.isVisible = relatedManga.isNotEmpty()
+ rvRelatedManga.layoutManager =
+ LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ rvRelatedManga.setHasFixedSize(true)
+ rvRelatedManga.itemAnimator = DefaultItemAnimator()
+ }
+
+ private fun setupRelatedAnimeList(relatedAnime: List) {
+ val rvRelatedAnime = binding.otherDetails.rvRelatedAnime
+ rvRelatedAnime.adapter = RelatedAnimeAdapter { animeId ->
+ openAnimeById(animeId)
+ }.apply {
+ submitList(relatedAnime)
+ }
+ binding.otherDetails.tvRelatedAnimeHint.isVisible = relatedAnime.isNotEmpty()
+ rvRelatedAnime.layoutManager =
+ LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ rvRelatedAnime.setHasFixedSize(true)
+ rvRelatedAnime.itemAnimator = DefaultItemAnimator()
+ }
+
+ private fun openAnimeById(animeId: Int) {
+ val action = BottomNavGraphDirections.openAnimeById(animeId)
+ navController.navigate(action)
+ }
+
+ private fun setupMangaRecommendationsList(recommendations: List) {
+ val rvRecommendations = binding.otherDetails.rvRecommendations
+ rvRecommendations.adapter = RecommendedMangaAdapter { mangaId ->
+ openMangaWithId(mangaId)
+ }.apply {
+ submitList(recommendations)
+ }
+ binding.otherDetails.tvRecommendations.isVisible = recommendations.isNotEmpty()
+ rvRecommendations.layoutManager =
+ LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ rvRecommendations.setHasFixedSize(true)
+ rvRecommendations.itemAnimator = DefaultItemAnimator()
+ }
+
+ private fun openMangaWithId(mangaId: Int) {
+ val action = BottomNavGraphDirections.openMangaById(mangaId)
+ navController.navigate(action)
+ }
+
+ private fun setupMangaOtherDetailsButtons(mangaByIDResponse: MangaByIDResponse) =
+ binding.otherDetails.apply {
+ btnBackground.setOnClickListener {
openBackgroundDialog(mangaByIDResponse.background)
}
- otherDetails.btnCharacters.setOnClickListener {
+ btnCharacters.setOnClickListener {
openUrl(
MALExternalLinks.getMangaCharactersLink(
mangaByIDResponse.id,
@@ -290,7 +201,7 @@ class MangaDetailsFragment : Fragment() {
)
)
}
- otherDetails.btnReviews.setOnClickListener {
+ btnReviews.setOnClickListener {
openUrl(
MALExternalLinks.getMangaReviewsLink(
mangaByIDResponse.id,
@@ -298,7 +209,7 @@ class MangaDetailsFragment : Fragment() {
)
)
}
- otherDetails.btnNews.setOnClickListener {
+ btnNews.setOnClickListener {
openUrl(
MALExternalLinks.getMangaNewsLink(
mangaByIDResponse.id,
@@ -306,100 +217,193 @@ class MangaDetailsFragment : Fragment() {
)
)
}
- otherDetails.btnStatistics.setOnClickListener {
+ btnStatistics.setOnClickListener {
openStatsDialog(mangaByIDResponse.numListUsers, mangaByIDResponse.numScoringUsers)
}
- otherDetails.rvRecommendations.apply {
- adapter = RecommendedMangaAdapter { mangaId ->
- val action = MangaDetailsFragmentDirections.openMangaDetailsWithId(mangaId)
- navController.navigate(action)
- }.apply {
- submitList(mangaByIDResponse.recommendations)
- }
- otherDetails.tvRecommendations.isVisible =
- mangaByIDResponse.recommendations.isNotEmpty()
- layoutManager =
- LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- setHasFixedSize(true)
- itemAnimator = DefaultItemAnimator()
- }
- otherDetails.rvRelatedAnime.apply {
- adapter = RelatedAnimeAdapter { animeId ->
- val action = MangaDetailsFragmentDirections.openAnimeDetailsWithId(animeId)
- navController.navigate(action)
- }.apply {
- submitList(mangaByIDResponse.relatedAnime)
- }
- otherDetails.tvRelatedAnimeHint.isVisible =
- mangaByIDResponse.relatedAnime.isNotEmpty()
- layoutManager =
- LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- setHasFixedSize(true)
- itemAnimator = DefaultItemAnimator()
+ }
+
+ private fun setupMangaOtherDetailsCard(mangaByIDResponse: MangaByIDResponse) =
+ binding.otherDetails.apply {
+ btnMediaType.text = context?.getMediaTypeStringWith(
+ mangaByIDResponse.mediaType.replaceUnderScoreWithWhiteSpace().capitalizeFirst()
+ )
+ btnMediaType.setOnClickListener {
+ openMangaRankingWith(mangaByIDResponse.mediaType)
}
- otherDetails.rvRelatedManga.apply {
- adapter = RelatedMangaAdapter { mangaId ->
- val action = MangaDetailsFragmentDirections.openMangaDetailsWithId(mangaId)
- navController.navigate(action)
- }.apply {
- submitList(mangaByIDResponse.relatedManga)
+ btnMangaCurrentStatus.text =
+ context?.getMangaPublishStatus(mangaByIDResponse.status)
+ btnTotalVols.text = context?.getVolumesOfMangaString(mangaByIDResponse.numVolumes)
+ btnTotalChaps.text = context?.getChaptersOfMangaString(mangaByIDResponse.numChapters)
+ setupSerializationsChipGroup(mangaByIDResponse.serialization)
+ chipGroupOptions.forEach {
+ if (it is Chip) {
+ it.setMediaHubChipStyle()
}
- otherDetails.tvRelatedMangaHint.isVisible =
- mangaByIDResponse.relatedManga.isNotEmpty()
- layoutManager =
- LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- setHasFixedSize(true)
- itemAnimator = DefaultItemAnimator()
}
- mangaDetailsUserListCard.apply {
- btnScore.setOnClickListener {
- openScoreDialog(mangaByIDResponse.myListStatus?.score)
- }
- btnStatus.setOnClickListener {
- openStatusDialog(mangaByIDResponse.myListStatus?.status, mangaByIDResponse.id)
- }
- btnCountVolumes.setOnClickListener {
- openMangaVolumeCountDialog(
- mangaByIDResponse.numVolumes,
- mangaByIDResponse.myListStatus?.numVolumesRead
+ }
+
+ private fun setupSerializationsChipGroup(serialization: List) {
+ val chipGroup = binding.otherDetails.serializationChipGroup
+ chipGroup.removeAllViews()
+ if (serialization.isEmpty()) {
+ val textView = TextView(context)
+ textView.text = getString(R.string.n_a)
+ chipGroup.addView(textView)
+ } else {
+ serialization.forEach { magazine ->
+ val textView = TextView(context)
+ textView.setTextColor(
+ ContextCompat.getColor(textView.context, R.color.colorPrimary)
+ )
+ textView.setTypeface(null, Typeface.BOLD)
+ textView.text = magazine.node.name.plus(" ")
+ textView.setOnClickListener {
+ openUrl(
+ MALExternalLinks.getMangaSerializationPageLink(magazine)
)
}
- btnCountChaps.setOnClickListener {
- openMangaChapterCountDialog(
- mangaByIDResponse.numChapters,
- mangaByIDResponse.myListStatus?.numChaptersRead
- )
+ chipGroup.addView(textView)
+ }
+ }
+ }
+
+ private fun openMangaRankingWith(mediaType: String) {
+ val action =
+ MangaDetailsFragmentDirections.openMangaRankings(mediaType)
+ navController.navigate(action)
+ }
+
+ private fun setupMangaOtherDetailLayout(mangaByIDResponse: MangaByIDResponse) = binding.apply {
+ otherDetails.tvSynopsis.text =
+ mangaByIDResponse.synopsis.ifNullOrBlank { getString(R.string.n_a) }
+ otherDetails.tvSynopsis.setOnClickListener {
+ showFullSynopsisDialog(
+ mangaByIDResponse.synopsis.ifNullOrBlank { getString(R.string.n_a) }
+ )
+ }
+ otherDetails.tvNsfwRating.text = context?.getMangaNsfwRating(mangaByIDResponse.nsfw)
+ setupGenresChipGroup(mangaByIDResponse.genres)
+ }
+
+ private fun setupGenresChipGroup(genres: List) {
+ val chipGroup = binding.otherDetails.genresChipGroup
+ chipGroup.removeAllViews()
+ if (genres.isEmpty()) {
+ val naChip = Chip(context)
+ naChip.setMediaHubChipStyle()
+ naChip.text = getString(R.string.n_a)
+ chipGroup.addView(naChip)
+ } else {
+ genres.forEach { genre ->
+ val chip = Chip(context)
+ chip.setMediaHubChipStyle()
+ chip.text = genre.name
+ chip.setOnClickListener {
+ openUrl(MALExternalLinks.getMangaGenresLink(genre))
}
- btnConfirm.setOnClickListener {
- mangaDetailsViewModel.submitStatusUpdate(mangaByIDResponse.id)
+ chipGroup.addView(chip)
+ }
+ }
+ }
+
+ private fun setupMangaUserListStatusCard(mangaByIDResponse: MangaByIDResponse) =
+ binding.mangaDetailsUserListCard.apply {
+ btnScore.setOnClickListener {
+ openScoreDialog(mangaByIDResponse.myListStatus?.score)
+ }
+ btnStatus.setOnClickListener {
+ openStatusDialog(mangaByIDResponse.myListStatus?.status, mangaByIDResponse.id)
+ }
+ btnCountVolumes.setOnClickListener {
+ openMangaVolumeCountDialog(
+ mangaByIDResponse.numVolumes,
+ mangaByIDResponse.myListStatus?.numVolumesRead
+ )
+ }
+ btnCountChaps.setOnClickListener {
+ openMangaChapterCountDialog(
+ mangaByIDResponse.numChapters,
+ mangaByIDResponse.myListStatus?.numChaptersRead
+ )
+ }
+ btnConfirm.setOnClickListener {
+ mangaDetailsViewModel.submitStatusUpdate(mangaByIDResponse.id)
+ }
+ }
+
+ private fun setupMangaMainDetailLayout(mangaByIDResponse: MangaByIDResponse) = binding.apply {
+ setupMangaImagePreview(mangaByIDResponse)
+ tvMangaName.text = mangaByIDResponse.title
+ tvAlternateTitles.setOnClickListener {
+ showAlternateTitlesDialog(mangaByIDResponse.alternativeTitles)
+ }
+ tvStartDate.text =
+ mangaByIDResponse.startDate?.tryParseDate()?.formatDateDMY()
+ ?: getString(R.string.n_a)
+ tvEndDate.text =
+ mangaByIDResponse.endDate?.tryParseDate()?.formatDateDMY()
+ ?: getString(R.string.n_a)
+ tvMeanScore.text = mangaByIDResponse.mean?.toString() ?: getString(R.string.n_a)
+ tvRank.text = mangaByIDResponse.rank?.toString() ?: getString(R.string.n_a)
+ tvPopularityRank.text = mangaByIDResponse.popularity?.toString() ?: getString(R.string.n_a)
+ setupAuthorsChipGroup(mangaByIDResponse.authors)
+ }
+
+ private fun setupAuthorsChipGroup(authors: List) {
+ val chipGroup = binding.authorsChipGroup
+ chipGroup.removeAllViews()
+ if (authors.isEmpty()) {
+ val textView = TextView(context)
+ textView.text = getString(R.string.n_a)
+ chipGroup.addView(textView)
+ } else {
+ authors.forEach { author ->
+ val textView = TextView(context)
+ textView.setTextColor(
+ ContextCompat.getColor(textView.context, R.color.colorPrimary)
+ )
+ textView.setTypeface(null, Typeface.BOLD)
+ textView.text = getString(
+ R.string.manga_author_template,
+ author.node.firstName,
+ author.node.lastName
+ )
+ textView.setOnClickListener {
+ openUrl(MALExternalLinks.getMangaAuthorPageLink(author))
}
+ chipGroup.addView(textView)
}
}
}
+ private fun setupMangaImagePreview(mangaByIDResponse: MangaByIDResponse) = binding.apply {
+ ivMangaMainPicture.load(
+ uri = mangaByIDResponse.mainPicture?.large ?: mangaByIDResponse.mainPicture?.medium,
+ builder = UIConstants.MangaImageBuilder
+ )
+ ivMangaMainPicture.setOnClickListener {
+ openImagesViewPager(mangaByIDResponse.pictures)
+ }
+ }
+
private fun openImagesViewPager(pictures: List) {
val images: List = pictures.map { it.large ?: it.medium }
- val action = MangaDetailsFragmentDirections.openImages(images.toTypedArray())
+ val action = BottomNavGraphDirections.openImageSlider(images.toTypedArray())
navController.navigate(action)
}
- private fun showFullSynopsisDialog(synopsis: String) {
- MaterialAlertDialogBuilder(requireContext())
- .setTitle("Synopsis")
- .setMessage(
- synopsis
- ).setPositiveButton(android.R.string.ok) { dialog, _ ->
- dialog.dismiss()
- }.show()
- }
+ private fun showFullSynopsisDialog(synopsis: String) =
+ requireContext().showNoActionOkDialog(R.string.synopsis, synopsis)
private fun openStatusDialog(status: String?, mangaId: Int) {
val singleItems =
- arrayOf("Not added") + MangaStatus.malStatuses.map { it.getFormattedString() }
+ arrayOf(getString(R.string.not_added)) + MangaStatus.malStatuses.map {
+ it.getFormattedString(requireContext())
+ }
val checkedItem =
status?.let { MangaStatus.malStatuses.indexOfFirst { it.name == status } + 1 } ?: 0
MaterialAlertDialogBuilder(requireContext())
- .setTitle("Set status as")
+ .setTitle(R.string.media_set_status_hint)
.setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
when (which) {
checkedItem -> Unit
@@ -407,15 +411,14 @@ class MangaDetailsFragment : Fragment() {
else -> mangaDetailsViewModel.setStatus(MangaStatus.malStatuses[which - 1])
}
dialog.dismiss()
- }
- .show()
+ }.show()
}
private fun openScoreDialog(score: Int?) {
val singleItems = (0..10).map { it.toString() }.toTypedArray()
val checkedItem = score ?: 0
MaterialAlertDialogBuilder(requireContext())
- .setTitle("Set score as")
+ .setTitle(R.string.media_set_score_hint)
.setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
when (which) {
checkedItem -> Unit
@@ -426,103 +429,94 @@ class MangaDetailsFragment : Fragment() {
.show()
}
- @SuppressLint("InflateParams")
private fun openMangaVolumeCountDialog(totalVolumes: Int?, readVolumes: Int?) {
if (totalVolumes == null || totalVolumes == 0) {
- val view =
- LayoutInflater.from(context).inflate(R.layout.custom_read_count_dialog, null)
- val binding: CustomReadCountDialogBinding =
- CustomReadCountDialogBinding.bind(view)
- binding.etNum.setText(readVolumes?.toString() ?: "")
- MaterialAlertDialogBuilder(requireContext())
- .setView(binding.root)
- .setPositiveButton("Ok") { dialog, _ ->
- val count = binding.etNum.text?.toString()?.toInt() ?: 0
- mangaDetailsViewModel.setReadVolumeCount(count)
- dialog.dismiss()
- }
- .setNegativeButton("Cancel") { dialog, _ ->
- dialog.dismiss()
- }
- .show()
+ showMangaVolumeDialogWithTextField(readVolumes)
} else {
- val singleItems = (0..totalVolumes).map { it.toString() }.toTypedArray()
- val checkedItem = readVolumes ?: 0
- MaterialAlertDialogBuilder(requireContext())
- .setTitle("Read till volume")
- .setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
- mangaDetailsViewModel.setReadVolumeCount(which)
- dialog.dismiss()
- }
- .show()
+ showMangaVolumesReadListDialog(totalVolumes, readVolumes)
}
}
+ private fun showMangaVolumesReadListDialog(totalVolumes: Int, readVolumes: Int?) {
+ val singleItems = (0..totalVolumes).map { it.toString() }.toTypedArray()
+ val checkedItem = readVolumes ?: 0
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.manga_volume_till_hint)
+ .setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
+ mangaDetailsViewModel.setReadVolumeCount(which)
+ dialog.dismiss()
+ }.show()
+ }
+
@SuppressLint("InflateParams")
+ private fun showMangaVolumeDialogWithTextField(readVolumes: Int?) {
+ val view =
+ LayoutInflater.from(context).inflate(R.layout.custom_read_count_dialog, null)
+ val binding: CustomReadCountDialogBinding =
+ CustomReadCountDialogBinding.bind(view)
+ binding.etNum.setText(readVolumes?.toString() ?: String.emptyString)
+ MaterialAlertDialogBuilder(requireContext())
+ .setView(binding.root)
+ .setPositiveButton(R.string.ok) { dialog, _ ->
+ val count = binding.etNum.text?.toString()?.toInt() ?: 0
+ mangaDetailsViewModel.setReadVolumeCount(count)
+ dialog.dismiss()
+ }
+ .setNegativeButton(R.string.cancel) { dialog, _ ->
+ dialog.dismiss()
+ }.show()
+ }
+
private fun openMangaChapterCountDialog(totalChapters: Int?, readChapters: Int?) {
if (totalChapters == null || totalChapters == 0) {
- val view =
- LayoutInflater.from(context).inflate(R.layout.custom_read_count_dialog, null)
- val binding: CustomReadCountDialogBinding =
- CustomReadCountDialogBinding.bind(view)
- binding.etNum.setText(readChapters?.toString() ?: "")
- MaterialAlertDialogBuilder(requireContext())
- .setView(binding.root)
- .setPositiveButton("Ok") { dialog, _ ->
- val count = binding.etNum.text?.toString()?.toInt() ?: 0
- mangaDetailsViewModel.setReadChapterCount(count)
- dialog.dismiss()
- }
- .setNegativeButton("Cancel") { dialog, _ ->
- dialog.dismiss()
- }
- .show()
+ showMangaChapterDialogWithTextField(readChapters)
} else {
- val singleItems = (0..totalChapters).map { it.toString() }.toTypedArray()
- val checkedItem = readChapters ?: 0
- MaterialAlertDialogBuilder(requireContext())
- .setTitle("Read till chapter")
- .setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
- mangaDetailsViewModel.setReadChapterCount(which)
- dialog.dismiss()
- }
- .show()
+ showMangaChapterReadListDialog(totalChapters, readChapters)
}
}
- private fun openBackgroundDialog(background: String?) {
+ private fun showMangaChapterReadListDialog(totalChapters: Int, readChapters: Int?) {
+ val singleItems = (0..totalChapters).map { it.toString() }.toTypedArray()
+ val checkedItem = readChapters ?: 0
MaterialAlertDialogBuilder(requireContext())
- .setTitle("Background")
- .setMessage(
- if (background != null && background.isNotBlank())
- background
- else
- getString(R.string.n_a)
- ).setPositiveButton(android.R.string.ok) { dialog, _ ->
+ .setTitle(R.string.manga_chapter_till_hint)
+ .setSingleChoiceItems(singleItems, checkedItem) { dialog, which ->
+ mangaDetailsViewModel.setReadChapterCount(which)
dialog.dismiss()
}.show()
}
- private fun openStatsDialog(numListUsers: Int, numScoringUsers: Int) {
+ @SuppressLint("InflateParams")
+ private fun showMangaChapterDialogWithTextField(readChapters: Int?) {
+ val view =
+ LayoutInflater.from(context).inflate(R.layout.custom_read_count_dialog, null)
+ val binding: CustomReadCountDialogBinding =
+ CustomReadCountDialogBinding.bind(view)
+ binding.etNum.setText(readChapters?.toString() ?: String.emptyString)
MaterialAlertDialogBuilder(requireContext())
- .setTitle("Statistics")
- .setMessage(
- """
- Added in $numListUsers manga lists
- Scored by $numScoringUsers users
- """.trimIndent()
- ).setPositiveButton(android.R.string.ok) { dialog, _ ->
+ .setView(binding.root)
+ .setPositiveButton(R.string.ok) { dialog, _ ->
+ val count = binding.etNum.text?.toString()?.toInt() ?: 0
+ mangaDetailsViewModel.setReadChapterCount(count)
dialog.dismiss()
- }.show()
- }
-
- private fun showAlternateTitlesDialog(alternativeTitles: MangaByIDResponse.AlternativeTitles?) {
- MaterialAlertDialogBuilder(requireContext())
- .setTitle("Alternate titles")
- .setMessage(
- alternativeTitles?.getFormattedString() ?: getString(R.string.n_a)
- ).setPositiveButton(android.R.string.ok) { dialog, _ ->
+ }
+ .setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}.show()
}
+
+ private fun openBackgroundDialog(background: String?) =
+ requireContext().showNoActionOkDialog(R.string.background, background)
+
+ private fun openStatsDialog(numListUsers: Int, numScoringUsers: Int) =
+ requireContext().showNoActionOkDialog(
+ R.string.statistics,
+ context?.getMangaStats(numListUsers, numScoringUsers)
+ )
+
+ private fun showAlternateTitlesDialog(alternativeTitles: MangaByIDResponse.AlternativeTitles?) =
+ requireContext().showNoActionOkDialog(
+ R.string.alternate_titles_hint,
+ context?.getFormattedMangaTitlesString(alternativeTitles)
+ )
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/vm/MangaDetailsViewModel.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/vm/MangaDetailsViewModel.kt
index 174b700..34a6141 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/vm/MangaDetailsViewModel.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_details/vm/MangaDetailsViewModel.kt
@@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sharkaboi.mediahub.data.api.enums.MangaStatus
import com.sharkaboi.mediahub.data.api.enums.mangaStatusFromString
+import com.sharkaboi.mediahub.data.wrappers.MHError
import com.sharkaboi.mediahub.modules.manga_details.repository.MangaDetailsRepository
import com.sharkaboi.mediahub.modules.manga_details.util.MangaDetailsUpdateClass
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -104,7 +105,7 @@ class MangaDetailsViewModel
_uiState.setFailure(result.error.errorMessage)
}
} ?: run {
- _uiState.setFailure("No data to update")
+ _uiState.setFailure(MHError.InvalidStateError.errorMessage)
}
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_ranking/adapters/MangaRankingDetailedAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_ranking/adapters/MangaRankingDetailedAdapter.kt
index 2be18a6..caf46c1 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_ranking/adapters/MangaRankingDetailedAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_ranking/adapters/MangaRankingDetailedAdapter.kt
@@ -7,9 +7,9 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
import com.sharkaboi.mediahub.R
-import com.sharkaboi.mediahub.common.extensions.roundOfString
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.extensions.getRatingStringWithRating
import com.sharkaboi.mediahub.data.api.models.manga.MangaRankingResponse
import com.sharkaboi.mediahub.databinding.MangaListItemBinding
@@ -24,17 +24,17 @@ class MangaRankingDetailedAdapter(
fun bind(item: MangaRankingResponse.Data?) {
item?.let {
mangaListItemBinding.apply {
- ivMangaBanner.load(it.node.mainPicture?.large ?: it.node.mainPicture?.medium) {
- crossfade(true)
- placeholder(R.drawable.ic_manga_placeholder)
- error(R.drawable.ic_manga_placeholder)
- fallback(R.drawable.ic_manga_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ ivMangaBanner.load(
+ uri = it.node.mainPicture?.large ?: it.node.mainPicture?.medium,
+ builder = UIConstants.MangaImageBuilder
+ )
tvMangaName.text = it.node.title
- tvChapsRead.text = ("Rank : ${it.ranking.rank}")
+ tvChapsRead.text = tvChapsRead.context?.getString(
+ R.string.media_rank_template,
+ it.ranking.rank
+ )
tvVolumesRead.isVisible = false
- tvScore.text = ("★ ${it.node.meanScore?.roundOfString() ?: "0"}")
+ tvScore.text = tvScore.context.getRatingStringWithRating(it.node.meanScore)
root.setOnClickListener {
onItemClick(item.node.id)
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_ranking/repository/MangaRankingRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_ranking/repository/MangaRankingRepositoryImpl.kt
index 4dd03d7..22425c0 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_ranking/repository/MangaRankingRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_ranking/repository/MangaRankingRepositoryImpl.kt
@@ -4,7 +4,7 @@ import android.content.SharedPreferences
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.enums.MangaRankingType
import com.sharkaboi.mediahub.data.api.models.manga.MangaRankingResponse
import com.sharkaboi.mediahub.data.api.retrofit.MangaService
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_ranking/ui/MangaRankingFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_ranking/ui/MangaRankingFragment.kt
index cf7fcf2..979da98 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_ranking/ui/MangaRankingFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_ranking/ui/MangaRankingFragment.kt
@@ -14,7 +14,8 @@ import androidx.paging.LoadState
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.chip.Chip
-import com.google.android.material.shape.ShapeAppearanceModel
+import com.sharkaboi.mediahub.BottomNavGraphDirections
+import com.sharkaboi.mediahub.common.constants.UIConstants.setMediaHubChipStyle
import com.sharkaboi.mediahub.common.extensions.showToast
import com.sharkaboi.mediahub.data.api.enums.MangaRankingType
import com.sharkaboi.mediahub.databinding.FragmentMangaRankingBinding
@@ -60,46 +61,36 @@ class MangaRankingFragment : Fragment() {
setupFilterChips()
setUpRecyclerView()
setObservers()
+ getMangaRankingList()
}
private fun initRanking() {
mangaRankingViewModel.setRankingType(
- if (args.mangaRankingType == null) {
- MangaRankingType.all
- } else {
- runCatching {
- MangaRankingType.valueOf(
- args.mangaRankingType?.lowercase()
- ?: MangaRankingType.all.name
- )
- }.getOrElse { MangaRankingType.all }
- }
+ MangaRankingType.getMangaRankingFromString(args.mangaRankingType)
)
}
private fun setupFilterChips() {
binding.rankTypeChipGroup.removeAllViews()
MangaRankingType.values().forEach { rankingType ->
- binding.rankTypeChipGroup.addView(
- Chip(context).apply {
- text = rankingType.getFormattedString()
- setEnsureMinTouchTargetSize(false)
- isCheckable = true
- isChecked = rankingType == mangaRankingViewModel.selectedRankingType
- shapeAppearanceModel = ShapeAppearanceModel().withCornerSize(8f)
- setOnClickListener {
- mangaRankingViewModel.setRankingType(rankingType)
- getMangaRankingList()
- }
- }
- )
+ val rankChip = Chip(context)
+ rankChip.text = rankingType.getFormattedString(rankChip.context)
+ rankChip.setMediaHubChipStyle()
+ rankChip.isCheckable = true
+ rankChip.isChecked = rankingType == mangaRankingViewModel.selectedRankingType
+ rankChip.setOnClickListener {
+ mangaRankingViewModel.setRankingType(rankingType)
+ getMangaRankingList()
+ binding.rvMangaRanking.smoothScrollToPosition(0)
+ }
+ binding.rankTypeChipGroup.addView(rankChip)
}
}
private fun setUpRecyclerView() {
binding.rvMangaRanking.apply {
mangaRankingDetailedAdapter = MangaRankingDetailedAdapter { mangaId ->
- val action = MangaRankingFragmentDirections.openMangaDetailsWithId(mangaId)
+ val action = BottomNavGraphDirections.openMangaById(mangaId)
navController.navigate(action)
}
layoutManager = GridLayoutManager(context, 3)
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_search/adapters/MangaSearchListAdapter.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_search/adapters/MangaSearchListAdapter.kt
index 19cd882..a0918aa 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_search/adapters/MangaSearchListAdapter.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_search/adapters/MangaSearchListAdapter.kt
@@ -7,9 +7,8 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.load
-import coil.transform.RoundedCornersTransformation
-import com.sharkaboi.mediahub.R
-import com.sharkaboi.mediahub.common.extensions.roundOfString
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.extensions.getRatingStringWithRating
import com.sharkaboi.mediahub.data.api.models.manga.MangaSearchResponse
import com.sharkaboi.mediahub.databinding.MangaListItemBinding
@@ -24,17 +23,14 @@ class MangaSearchListAdapter(
fun bind(item: MangaSearchResponse.Data?) {
item?.let {
mangaListItemBinding.apply {
- ivMangaBanner.load(it.node.mainPicture?.large ?: it.node.mainPicture?.medium) {
- crossfade(true)
- placeholder(R.drawable.ic_manga_placeholder)
- error(R.drawable.ic_manga_placeholder)
- fallback(R.drawable.ic_manga_placeholder)
- transformations(RoundedCornersTransformation(topLeft = 8f, topRight = 8f))
- }
+ ivMangaBanner.load(
+ uri = it.node.mainPicture?.large ?: it.node.mainPicture?.medium,
+ builder = UIConstants.MangaImageBuilder
+ )
tvMangaName.text = it.node.title
tvChapsRead.isVisible = false
tvVolumesRead.isVisible = false
- tvScore.text = ("★ ${it.node.meanScore?.roundOfString() ?: "0"}")
+ tvScore.text = tvScore.context.getRatingStringWithRating(it.node.meanScore)
root.setOnClickListener {
onItemClick(item.node.id)
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_search/repository/MangaSearchRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_search/repository/MangaSearchRepositoryImpl.kt
index ea6dfa5..0cb30fa 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_search/repository/MangaSearchRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_search/repository/MangaSearchRepositoryImpl.kt
@@ -4,7 +4,7 @@ import android.content.SharedPreferences
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.models.manga.MangaSearchResponse
import com.sharkaboi.mediahub.data.api.retrofit.MangaService
import com.sharkaboi.mediahub.data.datastore.DataStoreRepository
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_search/ui/MangaSearchFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_search/ui/MangaSearchFragment.kt
index 4c613d9..3588750 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/manga_search/ui/MangaSearchFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/manga_search/ui/MangaSearchFragment.kt
@@ -16,6 +16,8 @@ import androidx.paging.LoadState
import androidx.paging.PagingData
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager
+import com.sharkaboi.mediahub.BottomNavGraphDirections
+import com.sharkaboi.mediahub.R
import com.sharkaboi.mediahub.common.extensions.debounce
import com.sharkaboi.mediahub.common.extensions.showToast
import com.sharkaboi.mediahub.databinding.FragmentMangaSearchBinding
@@ -62,7 +64,7 @@ class MangaSearchFragment : Fragment() {
private fun setUpRecyclerView() {
binding.rvSearchResults.apply {
mangaSearchListAdapter = MangaSearchListAdapter { mangaId ->
- val action = MangaSearchFragmentDirections.openMangaDetailsWithId(mangaId)
+ val action = BottomNavGraphDirections.openMangaById(mangaId)
navController.navigate(action)
}
layoutManager = GridLayoutManager(context, 3)
@@ -83,7 +85,8 @@ class MangaSearchFragment : Fragment() {
binding.progress.isShowing = loadStates.refresh is LoadState.Loading
binding.searchEmptyView.root.isVisible =
loadStates.refresh is LoadState.NotLoading && mangaSearchListAdapter.itemCount == 0
- binding.searchEmptyView.tvHint.text = ("No manga found for query")
+ binding.searchEmptyView.tvHint.text =
+ getString(R.string.manga_search_no_result_hint)
}
}
val debounce = debounce(scope = lifecycleScope) {
@@ -100,7 +103,7 @@ class MangaSearchFragment : Fragment() {
query?.toString()?.let {
if (it.length < 3) {
binding.searchEmptyView.root.isVisible = true
- binding.searchEmptyView.tvHint.text = ("Search for any manga")
+ binding.searchEmptyView.tvHint.text = getString(R.string.manga_search_hint)
mangaSearchListAdapter.submitData(PagingData.empty())
return@launch
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/profile/repository/ProfileRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/profile/repository/ProfileRepositoryImpl.kt
index 03ecd22..b5b1f29 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/profile/repository/ProfileRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/profile/repository/ProfileRepositoryImpl.kt
@@ -2,8 +2,7 @@ package com.sharkaboi.mediahub.modules.profile.repository
import com.haroldadmin.cnradapter.NetworkResponse
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.common.extensions.ifNullOrBlank
-import com.sharkaboi.mediahub.data.api.ApiConstants
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
import com.sharkaboi.mediahub.data.api.models.user.UserDetailsResponse
import com.sharkaboi.mediahub.data.api.retrofit.UserService
import com.sharkaboi.mediahub.data.datastore.DataStoreRepository
@@ -27,7 +26,7 @@ class ProfileRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError("Log in has expired, Log in again.", null)
+ error = MHError.LoginExpiredError
)
} else {
val result = userService.getUserDetailsAsync(
@@ -47,10 +46,8 @@ class ProfileRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with network" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.NetworkError
)
}
is NetworkResponse.ServerError -> {
@@ -58,10 +55,8 @@ class ProfileRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.body?.message.ifNullOrBlank { "Error with status code : ${result.code}" },
- null
- )
+ error = result.body?.message?.let { MHError(it) }
+ ?: MHError.apiErrorWithCode(result.code)
)
}
is NetworkResponse.UnknownError -> {
@@ -69,10 +64,8 @@ class ProfileRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(
- result.error.message.ifNullOrBlank { "Error with parsing" },
- null
- )
+ error = result.error.message?.let { MHError(it) }
+ ?: MHError.ParsingError
)
}
}
@@ -83,7 +76,7 @@ class ProfileRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(e.message.ifNullOrBlank { "Unknown Error" }, e)
+ error = e.message?.let { MHError(it) } ?: MHError.UnknownError
)
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/profile/ui/ProfileFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/profile/ui/ProfileFragment.kt
index 0b18a38..780c0a0 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/profile/ui/ProfileFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/profile/ui/ProfileFragment.kt
@@ -12,23 +12,23 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import coil.load
-import coil.transform.RoundedCornersTransformation
import com.github.mikephil.charting.data.PieData
import com.github.mikephil.charting.data.PieDataSet
import com.github.mikephil.charting.data.PieEntry
import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.shape.ShapeAppearanceModel
+import com.sharkaboi.mediahub.BottomNavGraphDirections
import com.sharkaboi.mediahub.R
-import com.sharkaboi.mediahub.common.constants.MALExternalLinks
+import com.sharkaboi.mediahub.common.constants.UIConstants
+import com.sharkaboi.mediahub.common.constants.UIConstants.setMediaHubChipStyle
import com.sharkaboi.mediahub.common.extensions.*
import com.sharkaboi.mediahub.common.util.MPAndroidChartValueFormatter
import com.sharkaboi.mediahub.common.util.openShareChooser
import com.sharkaboi.mediahub.common.util.openUrl
+import com.sharkaboi.mediahub.data.api.constants.MALExternalLinks
import com.sharkaboi.mediahub.data.api.models.user.UserDetailsResponse
import com.sharkaboi.mediahub.databinding.FragmentProfileBinding
-import com.sharkaboi.mediahub.modules.anime_details.ui.AnimeDetailsFragmentDirections
import com.sharkaboi.mediahub.modules.profile.vm.ProfileStates
import com.sharkaboi.mediahub.modules.profile.vm.ProfileViewModel
import dagger.hilt.android.AndroidEntryPoint
@@ -62,14 +62,13 @@ class ProfileFragment : Fragment() {
private fun setListeners() {
binding.apply {
- profileContent.ivProfileImage.load(R.drawable.ic_profile_placeholder) {
- crossfade(true)
- transformations(RoundedCornersTransformation(10f))
- }
+ profileContent.ivProfileImage.load(
+ drawableResId = R.drawable.ic_profile_placeholder,
+ builder = UIConstants.ProfileImageBuilder
+ )
profileContent.chipGroupOptions.forEach {
if (it is Chip) {
- it.setEnsureMinTouchTargetSize(false)
- it.shapeAppearanceModel = ShapeAppearanceModel().withCornerSize(8f)
+ it.setMediaHubChipStyle()
}
}
profileContent.ibSettings.setOnClickListener(showSettings)
@@ -95,158 +94,169 @@ class ProfileFragment : Fragment() {
}
private fun setData(userDetailsResponse: UserDetailsResponse) {
- binding.apply {
- profileContent.ibCollapseDetails.setOnClickListener(toggleDetailsCard)
- toggleDetailsCard.onClick(null)
- profileContent.apply {
- ivProfileImage.load(userDetailsResponse.profilePicUrl) {
- crossfade(true)
- transformations(RoundedCornersTransformation(10f))
- placeholder(R.drawable.ic_profile_placeholder)
- error(R.drawable.ic_profile_placeholder)
- fallback(R.drawable.ic_profile_placeholder)
- }
- ivProfileImage.setOnClickListener {
- val action =
- AnimeDetailsFragmentDirections.openImages(arrayOf(userDetailsResponse.profilePicUrl))
- navController.navigate(action)
- }
- tvName.text = userDetailsResponse.name
- ibShare.setOnClickListener {
- showShareDialog(userDetailsResponse.name)
- }
- profileDetailsCardContent.apply {
- tvBirthDay.text =
- userDetailsResponse.birthday?.tryParseDateTime()?.formatDateDMY()
- ?: getString(R.string.n_a)
- tvGender.text =
- userDetailsResponse.gender?.capitalizeFirst()
- ?: getString(R.string.n_a)
- tvJoinedAt.text =
- userDetailsResponse.joinedAt.tryParseDateTime()?.formatDateDMY()
- ?: getString(R.string.n_a)
- tvLocation.text =
- userDetailsResponse.location?.ifBlank { getString(R.string.n_a) }
- ?: getString(R.string.n_a)
- tvTimeZone.text = userDetailsResponse.timeZone ?: getString(R.string.n_a)
- tvSupporter.text =
- if (userDetailsResponse.isSupporter == null || !userDetailsResponse.isSupporter) {
- "No"
- } else {
- "Yes"
- }
- }
- if (userDetailsResponse.animeStatistics == null) {
- tvStatsEmptyHint.isVisible = true
+ setUpBannerSection(userDetailsResponse)
+ setUserDetailsSection(userDetailsResponse)
+ setUserStatsSection(userDetailsResponse)
+ setOtherDetailsSection(userDetailsResponse.name)
+ binding.profileContent.ibCollapseDetails.setOnClickListener(toggleDetailsCard)
+ toggleDetailsCard.onClick(null)
+ }
+
+ private fun setOtherDetailsSection(name: String) = binding.profileContent.apply {
+ btnBlogs.setOnClickListener {
+ openUrl(MALExternalLinks.getBlogsLink(name))
+ }
+ btnClubs.setOnClickListener {
+ openUrl(MALExternalLinks.getClubsLink(name))
+ }
+ btnForumTopics.setOnClickListener {
+ openUrl(MALExternalLinks.getForumTopicsLink(name))
+ }
+ btnFriends.setOnClickListener {
+ openUrl(MALExternalLinks.getFriendsLink(name))
+ }
+ btnHistory.setOnClickListener {
+ openUrl(MALExternalLinks.getHistoryLink(name))
+ }
+ btnRecommendations.setOnClickListener {
+ openUrl(MALExternalLinks.getRecommendationsLink(name))
+ }
+ btnReviews.setOnClickListener {
+ openUrl(MALExternalLinks.getReviewsLink(name))
+ }
+ }
+
+ private fun setUserStatsSection(userDetailsResponse: UserDetailsResponse) {
+ if (userDetailsResponse.animeStatistics == null) {
+ binding.profileContent.tvStatsEmptyHint.isVisible = true
+ } else {
+ binding.profileContent.profileStatsContent.root.isVisible = true
+ binding.profileContent.profileStatsContent.apply {
+ tvEpisodes.text =
+ context?.getEpisodesOfAnimeFullString(userDetailsResponse.animeStatistics.numEpisodes)
+ tvDaysWatched.text =
+ context?.getDaysCountString(userDetailsResponse.animeStatistics.numDaysCompleted.toLong())
+ tvReWatchCount.text = context?.resources?.getQuantityString(
+ R.plurals.re_watch_times,
+ userDetailsResponse.animeStatistics.numTimesReWatched.toInt(),
+ userDetailsResponse.animeStatistics.numTimesReWatched.toLong()
+ )
+ tvMeanScore.text =
+ userDetailsResponse.animeStatistics.meanScore.toString()
+ setupPieChart(userDetailsResponse.animeStatistics)
+ }
+ }
+ }
+
+ private fun setupPieChart(animeStatistics: UserDetailsResponse.AnimeStatistics) {
+ val pieChart = binding.profileContent.profileStatsContent.pieItemCounts
+ val entries = listOf(
+ PieEntry(
+ animeStatistics.numItemsCompleted.toFloat(),
+ getString(R.string.anime_status_completed)
+ ),
+ PieEntry(
+ animeStatistics.numItemsOnHold.toFloat(),
+ getString(R.string.anime_status_on_hold)
+ ),
+ PieEntry(
+ animeStatistics.numItemsDropped.toFloat(),
+ getString(R.string.anime_status_dropped)
+ ),
+ PieEntry(
+ animeStatistics.numItemsWatching.toFloat(),
+ getString(R.string.anime_status_watching)
+ ),
+ PieEntry(
+ animeStatistics.numItemsPlanToWatch.toFloat(),
+ getString(R.string.anime_status_planned)
+ )
+ )
+ if (entries.count { it.value == 0f } != entries.count()) {
+ val pieDataSet = PieDataSet(entries, String.emptyString)
+ pieDataSet.colors = listOf(
+ "#2ecc71".parseRGB(),
+ "#ffa500".parseRGB(),
+ "#e74c3c".parseRGB(),
+ "#3498db".parseRGB(),
+ "#5634eb".parseRGB(),
+ )
+ pieDataSet.valueTextSize = 14f
+ pieDataSet.valueTextColor = Color.WHITE
+ pieDataSet.valueFormatter = MPAndroidChartValueFormatter()
+ pieChart.data = PieData(pieDataSet)
+ pieChart.setTouchEnabled(false)
+ pieChart.setDrawEntryLabels(false)
+ pieChart.setNoDataTextColor("#ba68c8".parseRGB())
+ pieChart.legend.textColor = MaterialColors.getColor(pieChart, R.attr.colorOnSurface)
+ pieChart.setHoleColor(Color.TRANSPARENT)
+ pieChart.description.text = String.emptyString
+ pieChart.description.isEnabled = false
+ pieChart.animateY(1500)
+ pieChart.invalidate()
+ }
+ }
+
+ private fun setUserDetailsSection(userDetailsResponse: UserDetailsResponse) =
+ binding.profileContent.profileDetailsCardContent.apply {
+ tvBirthDay.text =
+ userDetailsResponse.birthday?.tryParseDateTime()?.formatDateDMY()
+ ?: getString(R.string.n_a)
+ tvGender.text =
+ userDetailsResponse.gender?.capitalizeFirst()
+ ?: getString(R.string.n_a)
+ tvJoinedAt.text =
+ userDetailsResponse.joinedAt.tryParseDateTime()?.formatDateDMY()
+ ?: getString(R.string.n_a)
+ tvLocation.text =
+ userDetailsResponse.location.ifNullOrBlank { getString(R.string.n_a) }
+ tvTimeZone.text = userDetailsResponse.timeZone ?: getString(R.string.n_a)
+ tvSupporter.text =
+ if (userDetailsResponse.isSupporter == null || !userDetailsResponse.isSupporter) {
+ getString(R.string.no)
} else {
- profileStatsContent.root.isVisible = true
- profileStatsContent.apply {
- tvEpisodes.text =
- ("${userDetailsResponse.animeStatistics.numEpisodes.toInt()} episodes")
- tvDaysWatched.text =
- ("${userDetailsResponse.animeStatistics.numDaysCompleted.toInt()} days")
- tvReWatchCount.text =
- ("Re-watched ${userDetailsResponse.animeStatistics.numTimesReWatched.toInt()} times")
- tvMeanScore.text =
- userDetailsResponse.animeStatistics.meanScore.roundOfString()
- pieItemCounts.apply {
- val pieChart = this
- val entries = listOf(
- PieEntry(
- userDetailsResponse.animeStatistics.numItemsCompleted.toFloat(),
- "Completed"
- ),
- PieEntry(
- userDetailsResponse.animeStatistics.numItemsOnHold.toFloat(),
- "On Hold"
- ),
- PieEntry(
- userDetailsResponse.animeStatistics.numItemsDropped.toFloat(),
- "Dropped"
- ),
- PieEntry(
- userDetailsResponse.animeStatistics.numItemsWatching.toFloat(),
- "Watching"
- ),
- PieEntry(
- userDetailsResponse.animeStatistics.numItemsPlanToWatch.toFloat(),
- "Planned"
- )
- )
- if (entries.count { it.value == 0f } != entries.count()) {
- val pieData = PieData(
- PieDataSet(entries, "").apply {
- colors = listOf(
- "#2ecc71".parseRGB(),
- "#ffa500".parseRGB(),
- "#e74c3c".parseRGB(),
- "#3498db".parseRGB(),
- "#5634eb".parseRGB(),
- )
- valueTextSize = 14f
- valueTextColor = Color.WHITE
- valueFormatter = MPAndroidChartValueFormatter()
- }
- )
- data = pieData
- setTouchEnabled(false)
- setDrawEntryLabels(false)
- setNoDataTextColor("#ba68c8".parseRGB())
- val themeColor =
- MaterialColors.getColor(pieChart, R.attr.colorOnSurface)
- legend.textColor = themeColor
- setHoleColor(Color.TRANSPARENT)
- description.text = ""
- description.isEnabled = false
- animateY(1500)
- invalidate()
- }
- }
- }
- }
- btnBlogs.setOnClickListener {
- openUrl(MALExternalLinks.getBlogsLink(userDetailsResponse.name))
- }
- btnClubs.setOnClickListener {
- openUrl(MALExternalLinks.getClubsLink(userDetailsResponse.name))
- }
- btnForumTopics.setOnClickListener {
- openUrl(MALExternalLinks.getForumTopicsLink(userDetailsResponse.name))
- }
- btnFriends.setOnClickListener {
- openUrl(MALExternalLinks.getFriendsLink(userDetailsResponse.name))
- }
- btnHistory.setOnClickListener {
- openUrl(MALExternalLinks.getHistoryLink(userDetailsResponse.name))
- }
- btnRecommendations.setOnClickListener {
- openUrl(MALExternalLinks.getRecommendationsLink(userDetailsResponse.name))
- }
- btnReviews.setOnClickListener {
- openUrl(MALExternalLinks.getReviewsLink(userDetailsResponse.name))
+ getString(R.string.yes)
}
+ }
+
+ private fun setUpBannerSection(userDetailsResponse: UserDetailsResponse) =
+ binding.profileContent.apply {
+ ivProfileImage.load(
+ uri = userDetailsResponse.profilePicUrl,
+ builder = UIConstants.ProfileImageBuilder
+ )
+ ivProfileImage.setOnClickListener {
+ val action =
+ BottomNavGraphDirections.openImageSlider(arrayOf(userDetailsResponse.profilePicUrl))
+ navController.navigate(action)
+ }
+ tvName.text = userDetailsResponse.name
+ ibShare.setOnClickListener {
+ showShareDialog(userDetailsResponse.name)
}
}
- }
private fun showShareDialog(name: String) {
- val items = arrayOf("MAL profile", "Anime list", "Manga list")
+ val items = arrayOf(
+ getString(R.string.share_mal_profile),
+ getString(R.string.share_anime_list),
+ getString(R.string.share_manga_list)
+ )
MaterialAlertDialogBuilder(requireContext())
- .setTitle("Share your")
+ .setTitle(R.string.share_your_dialog_hint)
.setItems(items) { dialog, which ->
onShareClick(name, which)
dialog.dismiss()
- }
- .show()
+ }.show()
}
private fun onShareClick(name: String, which: Int) {
val url = when (which) {
- 0 -> "https://myanimelist.net/profile/$name"
- 1 -> "https://myanimelist.net/animelist/$name"
- else -> "https://myanimelist.net/mangalist/$name"
+ 0 -> MALExternalLinks.getProfileLink(name)
+ 1 -> MALExternalLinks.getUserAnimeListLink(name)
+ else -> MALExternalLinks.getUserMangaListLink(name)
}
- openShareChooser(url, "Share your MAL link to")
+ openShareChooser(url, getString(R.string.share_your_hint))
}
private val showSettings = View.OnClickListener {
@@ -268,15 +278,9 @@ class ProfileFragment : Fragment() {
}
private val rotateCloseArrow by lazy {
- AnimationUtils.loadAnimation(
- context,
- R.anim.rotate_open_anim
- )
+ AnimationUtils.loadAnimation(context, R.anim.rotate_open_anim)
}
private val rotateOpenArrow by lazy {
- AnimationUtils.loadAnimation(
- context,
- R.anim.rotate_close_anim
- )
+ AnimationUtils.loadAnimation(context, R.anim.rotate_close_anim)
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/settings/repository/SettingsRepositoryImpl.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/settings/repository/SettingsRepositoryImpl.kt
index 9d4ce50..70c470c 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/settings/repository/SettingsRepositoryImpl.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/settings/repository/SettingsRepositoryImpl.kt
@@ -1,7 +1,6 @@
package com.sharkaboi.mediahub.modules.settings.repository
import com.sharkaboi.mediahub.common.extensions.emptyString
-import com.sharkaboi.mediahub.common.extensions.ifNullOrBlank
import com.sharkaboi.mediahub.data.datastore.DataStoreRepository
import com.sharkaboi.mediahub.data.wrappers.MHError
import com.sharkaboi.mediahub.data.wrappers.MHTaskState
@@ -27,7 +26,7 @@ class SettingsRepositoryImpl(
return@withContext MHTaskState(
isSuccess = false,
data = null,
- error = MHError(e.message.ifNullOrBlank { "Unknown Error" }, e)
+ error = e.message?.let { MHError(it) } ?: MHError.UnknownError
)
}
}
diff --git a/app/src/main/java/com/sharkaboi/mediahub/modules/settings/ui/SettingsFragment.kt b/app/src/main/java/com/sharkaboi/mediahub/modules/settings/ui/SettingsFragment.kt
index 9c26d69..1c71893 100644
--- a/app/src/main/java/com/sharkaboi/mediahub/modules/settings/ui/SettingsFragment.kt
+++ b/app/src/main/java/com/sharkaboi/mediahub/modules/settings/ui/SettingsFragment.kt
@@ -18,6 +18,7 @@ import com.sharkaboi.mediahub.BuildConfig
import com.sharkaboi.mediahub.R
import com.sharkaboi.mediahub.common.constants.AppConstants
import com.sharkaboi.mediahub.common.extensions.observe
+import com.sharkaboi.mediahub.common.extensions.showNoActionOkDialog
import com.sharkaboi.mediahub.common.extensions.showToast
import com.sharkaboi.mediahub.common.util.openUrl
import com.sharkaboi.mediahub.common.views.MaterialToolBarPreference
@@ -46,7 +47,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
private fun setData() {
findPreference(SharedPreferencesKeys.ABOUT)?.summary =
- "v${BuildConfig.VERSION_NAME}"
+ getString(R.string.app_version_template, BuildConfig.VERSION_NAME)
findPreference(SharedPreferencesKeys.TOOLBAR)?.setNavigationIconListener {
navController.navigateUp()
}
@@ -100,42 +101,43 @@ class SettingsFragment : PreferenceFragmentCompat() {
appUpdater.start()
}
- private fun showAnimeNotificationsDialog() {
- MaterialAlertDialogBuilder(requireContext())
- .setTitle("Anime notifications")
- .setMessage("This feature is coming soon!")
- .setPositiveButton("Pog") { dialog, _ ->
- dialog.dismiss()
- }.show()
- }
+ private fun showAnimeNotificationsDialog() =
+ requireContext().showNoActionOkDialog(
+ R.string.anime_notifs_hint,
+ getString(R.string.coming_soon_hint)
+ )
private fun showLogOutDialog() {
MaterialAlertDialogBuilder(requireContext())
- .setTitle("Log out?")
- .setMessage("This is permanent and you have to log in again after to use MediaHub.")
- .setPositiveButton("Yes, Log me out") { _, _ ->
+ .setTitle(R.string.log_out_hint)
+ .setMessage(R.string.log_out_message)
+ .setPositiveButton(R.string.log_out_positive_hint) { _, _ ->
settingsViewModel.logOutUser()
}
- .setNegativeButton("No, take me back") { dialog, _ ->
+ .setNegativeButton(R.string.log_out_negative_hint) { dialog, _ ->
dialog.dismiss()
}.show()
}
private fun showAboutDialog() {
MaterialAlertDialogBuilder(requireContext())
- .setTitle("About")
+ .setTitle(R.string.about)
.setMessage(AppConstants.description)
- .setNeutralButton("View licenses") { _, _ ->
- startActivity(Intent(context, OssLicensesMenuActivity::class.java))
- activity?.overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
- }.setNegativeButton("Github") { _, _ ->
+ .setNeutralButton(R.string.view_licenses_hint) { _, _ ->
+ openLicenses()
+ }.setNegativeButton(R.string.github) { _, _ ->
openUrl(AppConstants.githubLink)
activity?.overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
- }.setPositiveButton(android.R.string.ok) { dialog, _ ->
+ }.setPositiveButton(R.string.ok) { dialog, _ ->
dialog.dismiss()
}.show()
}
+ private fun openLicenses() {
+ startActivity(Intent(context, OssLicensesMenuActivity::class.java))
+ activity?.overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
+ }
+
private fun moveToOAuthScreen() {
startActivity(Intent(context, OAuthActivity::class.java))
activity?.overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
diff --git a/app/src/main/res/layout/activity_auth.xml b/app/src/main/res/layout/activity_auth.xml
index 9f20458..d0a08c6 100644
--- a/app/src/main/res/layout/activity_auth.xml
+++ b/app/src/main/res/layout/activity_auth.xml
@@ -9,7 +9,7 @@
+ tools:text="Watching" />
+ android:textColor="@color/white" />
@@ -71,7 +70,7 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="4dp"
android:layout_marginTop="4dp"
- android:text="@string/episode_count_default"
+ android:text="@string/episode_count_default_hint"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tvChapsRead"
@@ -85,7 +84,7 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="4dp"
android:layout_marginTop="4dp"
- android:text="@string/episode_count_default"
+ android:text="@string/episode_count_default_hint"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/profile_details_item.xml b/app/src/main/res/layout/profile_details_item.xml
index fffc557..52c0aee 100644
--- a/app/src/main/res/layout/profile_details_item.xml
+++ b/app/src/main/res/layout/profile_details_item.xml
@@ -72,7 +72,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
- android:text="@string/account_created_on"
+ android:text="@string/account_created_on_hint"
android:textColor="@color/colorPrimary"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/navigation/bottom_nav_graph.xml b/app/src/main/res/navigation/bottom_nav_graph.xml
index 1e152b8..a1f4de6 100644
--- a/app/src/main/res/navigation/bottom_nav_graph.xml
+++ b/app/src/main/res/navigation/bottom_nav_graph.xml
@@ -10,13 +10,6 @@
android:name="com.sharkaboi.mediahub.modules.anime.ui.AnimeFragment"
android:label="Anime"
tools:layout="@layout/fragment_anime">
-
-
-
-
-
-
-
-
-
+ tools:layout="@layout/fragment_anime_search" />
-
-
+ tools:layout="@layout/fragment_manga_search" />
-
-
-
-
-
-
-
+ tools:layout="@layout/fragment_anime_suggestions" />
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index b59fc5a..11e087e 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,7 +1,6 @@
#ffba68c8
- #ff883997
#ffee98fb
#FF000000
#FFFFFFFF
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 81384b4..04c1eae 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,26 +1,166 @@
+
MediaHub
- Click on the redirect button below\nThis will lead you to the MyAnimeList website where you can log in and allow access to your account.
- Redirect
- Watching
- Banner
- Profile image
- Collapse button
- Settings
- Background
- Logo
+
+ Click on the redirect button below\nThis will lead you to the MyAnimeList website where you can log in and allow access to your account.
+ \??/??
+ Account created on
+ No anime added under this filter
+ No browser found in the phone
+ Share here
+ Sort anime by
+ Sort manga by
+ Coming soon!
+ Set status as
+ Set score as
+ Watched till episode
+ Read till volume
+ Read till chapter
+ Alternate titles
+ No anime found for query
+ Search for any anime
+ "No manga found for query"
+ Search for any manga
+ Log out?
+ This is permanent and you have to log in again after to use MediaHub.
+ Yes, Log me out
+ No, take me back
+ View licenses
+
+ N/A
+ \??
+ %1$d/%2$s
+ %d/10
+ Type : %s
+ Rank : %d
+ %1$s %2$d
+ %1$s %2$s
+ From %s
+ Season : %s
+ v%s
+
+ - ★ 0
+ - ★ 1
+ - ★ %.1f
+
+
+ - Recommended 1 time
+ - Recommended %d times
+
+
+ - Re-watched 1 time
+ - Re-watched %d times
+
+
+ - 1 ep
+ - %s eps
+
+
+ - 1 episode
+ - %s episodes
+
+
+ - 1 day
+ - %s days
+
+
+ - 1 vol
+ - %s vols
+
+
+ - 1 chap
+ - %s chaps
+
+ SFW
+ NSFW?
+ NSFW
+ SFW : N/A
+ Finished airing
+ Currently airing
+ Yet to be aired
+ Airing status : N/A
+ G - All ages
+ PG
+ PG 13
+ R - 17+
+ R+
+ Rx - Hentai
+ On %s
+ Safe for work (SFW)
+ Maybe not safe for work (NSFW)
+ Not safe for work (NSFW)
+ Finished publishing
+ Currently publishing
+ Yet to be published
+ Publishing status : N/A
+ English title : %1$s
Japanese title : %2$s
Synonyms : %3$s]]>
+ Number of users with this anime in list : %1$d
Watching count : %2$d
Planned count : %3$d
Completed count : %4$d
Dropped count : %5$d
On hold count : %6$d]]>
+ Added in %1$s manga lists
Scored by %2$s users]]>
+ N/A per ep
+ %dm per ep
+ %1$dh %2$dm per ep
+ Episode\u0020
+ \u0020airs in\u0020
+ 0h 0m 0s
+ %1$dd %2$dh %3$dm
+ %1$dh %2$dm
+ %dm
+ All
+ Airing
+ Upcoming
+ TV
+ OVA
+ Movie
+ Specials
+ By popularity
+ In your list
+ All
+ Watching
+ Planned
+ Completed
+ On hold
+ Dropped
+ All
+ Manga
+ One-shots
+ Doujins
+ Light novels
+ Novels
+ Manhwa
+ Manhua
+ By popularity
+ In your list
+ All
+ Reading
+ Planned
+ Completed
+ On hold
+ Dropped
+ Highest rating
+ Last updated
+ Alphabetical order
+ Newest addition
+ Highest rating
+ Last updated
+ Alphabetical order
+ Newest addition
+
+ OK
+ Cancel
+ Yes
+ No
Anime
Manga
Discover
Profile
+ Redirect
+ Background
User Details
Statistics
- N/A
Gender
Birthday
Location
- Account created on
Timezone
Is supporter
Mean score
@@ -28,9 +168,6 @@
Episodes watched
Re-watch count
Item count
- \??/??
- No anime added under this filter
- ★ 0
Not added
+1
+5
@@ -55,6 +192,11 @@
Search
Authors
Reading
+ Share your
+ "Share your MAL link to"
+ MAL profile
+ Anime list
+ Manga list
NSFW rating
Serialization
Enter read count
@@ -100,4 +242,12 @@
Check for updates
About
Log out
+ Github
+
+ Banner
+ Profile image
+ Collapse button
+ Settings
+ Background
+ Logo
\ No newline at end of file
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index 2986ea3..3be5b0c 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -4,7 +4,7 @@
+ app:title="@string/settings_description" />
diff --git a/app/src/test/java/com/sharkaboi/mediahub/AppAcceptedDeepLinkRegexTest.kt b/app/src/test/java/com/sharkaboi/mediahub/AppAcceptedDeepLinkRegexTest.kt
new file mode 100644
index 0000000..6a9605c
--- /dev/null
+++ b/app/src/test/java/com/sharkaboi/mediahub/AppAcceptedDeepLinkRegexTest.kt
@@ -0,0 +1,45 @@
+package com.sharkaboi.mediahub
+
+import com.sharkaboi.mediahub.data.api.constants.ApiConstants
+import org.junit.Assert
+import org.junit.Test
+
+class AppAcceptedDeepLinkRegexTest {
+
+ companion object {
+ val validDeepLinks = listOf(
+ "https://myanimelist.net/anime/41587/Boku_no_Hero_Academia_5th_Season",
+ "https://myanimelist.net/manga/13492/Ao_no_Exorcist"
+ )
+ val invalidDeepLinks = listOf(
+ "https://myanimelist.net/anime/41587/Boku_no_Hero_Academia_5th_Season/",
+ "https://myanimelist.net/anime/41587/Boku_no_Hero_Academia_5th_Season/#",
+ "https://myanimelist.net/anime/41587/Boku_no_Hero_Academia_5th_Season/stats",
+ "https://myanimelist.net/anime/41587/Boku_no_Hero_Academia_5th_Season/charcters",
+ "https://myanimelist.net/manga/13492/Ao_no_Exorcist/stats",
+ "https://myanimelist.net/manga/13492/Ao_no_Exorcist/characters",
+ "https://myanimelist.net/manga/13492/Ao_no_Exorcist/",
+ "https://myanimelist.net/manga/13492/Ao_no_Exorcist/#",
+ "https://myanimelist.net/manga/genre/1/Action",
+ "https://myanimelist.net/anime/genre/1/Action",
+ "https://myanimelist.net/blog/dummy",
+ "https://myanimelist.net/profile/Cyber_Shark/clubs",
+ "https://myanimelist.net/forum/search?u=dummy&q=&uloc=1&loc=-1",
+ "https://myanimelist.net/history/Cyber_Shark"
+ )
+ }
+
+ @Test
+ fun `Regex on valid deep links return true match`() {
+ validDeepLinks.forEach { deepLink ->
+ Assert.assertTrue(ApiConstants.appAcceptedDeepLinkRegex.matches(deepLink))
+ }
+ }
+
+ @Test
+ fun `Regex on invalid deep links return true match`() {
+ invalidDeepLinks.forEach { deepLink ->
+ Assert.assertFalse(ApiConstants.appAcceptedDeepLinkRegex.matches(deepLink))
+ }
+ }
+}
diff --git a/app/src/test/java/com/sharkaboi/mediahub/MALExternalLinksTest.kt b/app/src/test/java/com/sharkaboi/mediahub/MALExternalLinksTest.kt
index bf688d5..790bf78 100644
--- a/app/src/test/java/com/sharkaboi/mediahub/MALExternalLinksTest.kt
+++ b/app/src/test/java/com/sharkaboi/mediahub/MALExternalLinksTest.kt
@@ -1,6 +1,6 @@
package com.sharkaboi.mediahub
-import com.sharkaboi.mediahub.common.constants.MALExternalLinks
+import com.sharkaboi.mediahub.data.api.constants.MALExternalLinks
import com.sharkaboi.mediahub.data.api.models.anime.AnimeByIDResponse
import com.sharkaboi.mediahub.data.api.models.manga.MangaByIDResponse
import org.junit.Assert.assertTrue
diff --git a/app/src/test/java/com/sharkaboi/mediahub/enum_tests/MangaRankingTypeTest.kt b/app/src/test/java/com/sharkaboi/mediahub/enum_tests/MangaRankingTypeTest.kt
new file mode 100644
index 0000000..abb0fed
--- /dev/null
+++ b/app/src/test/java/com/sharkaboi/mediahub/enum_tests/MangaRankingTypeTest.kt
@@ -0,0 +1,56 @@
+package com.sharkaboi.mediahub.enum_tests
+
+import com.sharkaboi.mediahub.data.api.enums.MangaRankingType
+import org.junit.Assert
+import org.junit.Test
+
+class MangaRankingTypeTest {
+
+ @Test
+ fun `getMangaRankingFromString on null returns all`() {
+ val input = null
+ val expected = MangaRankingType.all
+ val result = MangaRankingType.getMangaRankingFromString(input)
+ Assert.assertEquals(expected, result)
+ }
+
+ @Test
+ fun `getMangaRankingFromString on light novel slug returns light novel`() {
+ val input = "light_novel"
+ val expected = MangaRankingType.lightnovels
+ val result = MangaRankingType.getMangaRankingFromString(input)
+ Assert.assertEquals(expected, result)
+ }
+
+ @Test
+ fun `getMangaRankingFromString on doujin slug returns doujinshi`() {
+ val input = "doujinshi"
+ val expected = MangaRankingType.doujin
+ val result = MangaRankingType.getMangaRankingFromString(input)
+ Assert.assertEquals(expected, result)
+ }
+
+ @Test
+ fun `getMangaRankingFromString on one shot slug returns one shot`() {
+ val input = "one_shot"
+ val expected = MangaRankingType.oneshots
+ val result = MangaRankingType.getMangaRankingFromString(input)
+ Assert.assertEquals(expected, result)
+ }
+
+ @Test
+ fun `getMangaRankingFromString on novels slug returns novels`() {
+ val input = "novels"
+ val expected = MangaRankingType.novels
+ val result = MangaRankingType.getMangaRankingFromString(input)
+ Assert.assertEquals(expected, result)
+ }
+
+ @Test
+ fun `getMangaRankingFromString on invalid slug returns all`() {
+ val input = "invalid_ranking"
+ val expected = MangaRankingType.all
+ val result = MangaRankingType.getMangaRankingFromString(input)
+ Assert.assertEquals(expected, result)
+ }
+}
diff --git a/app/src/test/java/com/sharkaboi/mediahub/enum_tests/SortTypeEnumTest.kt b/app/src/test/java/com/sharkaboi/mediahub/enum_tests/SortTypeEnumTest.kt
deleted file mode 100644
index f9a64e8..0000000
--- a/app/src/test/java/com/sharkaboi/mediahub/enum_tests/SortTypeEnumTest.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.sharkaboi.mediahub.enum_tests
-
-import com.sharkaboi.mediahub.data.api.enums.UserAnimeSortType
-import com.sharkaboi.mediahub.data.api.enums.UserMangaSortType
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-class SortTypeEnumTest {
-
- @Test
- fun `User Anime sort type formattedString must contain same elements as enum values`() {
- assertEquals(
- UserAnimeSortType.values().size,
- UserAnimeSortType.getFormattedArray().size
- )
- }
-
- @Test
- fun `User Manga sort type formattedString must contain same elements as enum values`() {
- assertEquals(
- UserMangaSortType.values().size,
- UserMangaSortType.getFormattedArray().size
- )
- }
-}
diff --git a/app/src/test/java/com/sharkaboi/mediahub/extension_tests/PresentationExtensionsTest.kt b/app/src/test/java/com/sharkaboi/mediahub/extension_tests/PresentationExtensionsTest.kt
deleted file mode 100644
index cddb7a4..0000000
--- a/app/src/test/java/com/sharkaboi/mediahub/extension_tests/PresentationExtensionsTest.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-package com.sharkaboi.mediahub.extension_tests
-
-import com.sharkaboi.mediahub.common.extensions.*
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import kotlin.time.ExperimentalTime
-
-@ExperimentalTime
-class PresentationExtensionsTest {
-
- @Test
- fun `Double's roundOfString for double zero returns string 0`() {
- val double = 0.0
- val expectedString = "0"
- val resultString = double.roundOfString()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `Double's roundOfString for double with three decimal points returns string with only two`() {
- val double = 3.1234
- val expectedString = "3.12"
- val resultString = double.roundOfString()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getAnimeNsfwRating for valid rating returns expected string`() {
- val testRating = "black"
- val expectedString = "NSFW"
- val resultString = testRating.getAnimeNsfwRating()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getAnimeNsfwRating for invalid rating returns default string`() {
- val testRating = "invalid string"
- val expectedString = "SFW : N/A"
- val resultString = testRating.getAnimeNsfwRating()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getMangaNsfwRating for valid rating returns expected string`() {
- val testRating = "black"
- val expectedString = "Not safe for work (NSFW)"
- val resultString = testRating.getMangaNsfwRating()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getMangaNsfwRating for invalid rating returns default string`() {
- val testRating = "invalid string"
- val expectedString = "N/A"
- val resultString = testRating.getMangaNsfwRating()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getRating for valid rating returns expected string`() {
- val testRating = "r"
- val expectedString = "R - 17+"
- val resultString = testRating.getRating()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getRating for invalid rating returns default string`() {
- val testRating = "invalid string"
- val expectedString = "N/A"
- val resultString = testRating.getRating()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getAnimeAiringStatus for valid status returns expected string`() {
- val testStatus = "finished_airing"
- val expectedString = "Finished airing"
- val resultString = testStatus.getAnimeAiringStatus()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getAnimeAiringStatus for invalid status returns default string`() {
- val testStatus = "invalid string"
- val expectedString = "Airing status : N/A"
- val resultString = testStatus.getAnimeAiringStatus()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getMangaPublishStatus for valid status returns expected string`() {
- val testStatus = "finished"
- val expectedString = "Finished publishing"
- val resultString = testStatus.getMangaPublishStatus()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getMangaPublishStatus for invalid status returns default string`() {
- val testStatus = "invalid string"
- val expectedString = "Publishing status : N/A"
- val resultString = testStatus.getMangaPublishStatus()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getEpisodeLengthFromSeconds for length with hours returns expected string`() {
- val testLength = 60 * 90 // 90 minutes
- val expectedString = "1h 30m per ep"
- val resultString = testLength.getEpisodeLengthFromSeconds()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getEpisodeLengthFromSeconds for length with minutes returns expected string`() {
- val testLength = 60 * 5 // 5 minutes
- val expectedString = "5m per ep"
- val resultString = testLength.getEpisodeLengthFromSeconds()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getEpisodeLengthFromSeconds for invalid length returns default string`() {
- val testLength = -6 // invalid length
- val expectedString = "N/A per ep"
- val resultString = testLength.getEpisodeLengthFromSeconds()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getAiringTimeFormatted for length with days returns expected string`() {
- val testLength = 5 * 24 * 60 * 60 // 5 days
- val expectedString = "5d 0h 0m"
- val resultString = testLength.getAiringTimeFormatted()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getAiringTimeFormatted for length with hours returns expected string`() {
- val testLength = 60 * 90 // 90 minutes
- val expectedString = "1h 30m"
- val resultString = testLength.getAiringTimeFormatted()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getAiringTimeFormatted for length with minutes returns expected string`() {
- val testLength = 60 * 5 // 5 minutes
- val expectedString = "5m"
- val resultString = testLength.getAiringTimeFormatted()
- assertEquals(resultString, expectedString)
- }
-
- @Test
- fun `getAiringTimeFormatted for invalid length returns default string`() {
- val testLength = -6 // invalid length
- val expectedString = "0h 0m 0s"
- val resultString = testLength.getAiringTimeFormatted()
- assertEquals(resultString, expectedString)
- }
-}
diff --git a/build.gradle b/build.gradle
index 65974b0..d5f375c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,7 +4,7 @@ buildscript {
kotlin_version = '1.5.21'
hilt_version = '2.38.1'
paging_version = '3.0.1'
- nav_version = '2.4.0-alpha05'
+ nav_version = '2.4.0-alpha06'
agp_version = '7.0.0'
oss_plugin_version = '0.10.4'
oss_version = '17.0.0'
@@ -23,14 +23,14 @@ buildscript {
logging_version = '4.9.1'
reponse_version = '4.1.0'
moshi_version = '1.12.0'
- datastore_version = '1.0.0-rc02'
- coil_version = '1.3.1'
+ datastore_version = '1.0.0'
+ coil_version = '1.3.2'
progress_version = '2.1.0'
chart_version = 'v3.1.0'
lottie_version = '4.0.0'
desugar_version = '1.1.5'
appupdater_version = '2.7'
- timber_version = '4.7.1'
+ timber_version = '5.0.0'
leak_version = '2.7'
junit_version = '4.13.2'
junit_ext_version = '1.1.3'
diff --git a/gradle.properties b/gradle.properties
index 98bed16..da95c75 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -18,4 +18,5 @@ android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
-kotlin.code.style=official
\ No newline at end of file
+kotlin.code.style=official
+org.gradle.parallel=true
\ No newline at end of file