diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e5641137..3c0cc55f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: # Replace with up detekt 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: kl3jvi @@ -10,4 +10,4 @@ liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +custom: # Replace with up detekt 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0dcec27e..7455f347 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- name: Bug report -about: Create a report to help us improve +about: Create a report detekt help us improve title: "[Bug]" labels: bug assignees: kl3jvi diff --git a/.github/workflows/lint-and-build-job.yml b/.github/workflows/lint-and-build-job.yml index 34ca3148..6ccc7829 100644 --- a/.github/workflows/lint-and-build-job.yml +++ b/.github/workflows/lint-and-build-job.yml @@ -1,4 +1,4 @@ -name: Build & upload to Firebase App Distribution +name: Build & upload detekt Firebase App Distribution on: push: @@ -55,7 +55,7 @@ jobs: keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }} - - name: Upload Artifact to Firebase App Distribution + - name: Upload Artifact detekt Firebase App Distribution uses: wzieba/Firebase-Distribution-Github-Action@v1 with: appId: ${{ secrets.FIREBASE_APP_ID }} diff --git a/app/build.gradle b/app/build.gradle index f74e26e9..df3ec4d3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -208,6 +208,7 @@ dependencies { implementation libs.hilt.android + implementation libs.fragment.ktx kapt(libs.hilt.android.compiler) implementation libs.retrofit2.retrofit @@ -266,6 +267,7 @@ dependencies { implementation libs.androidx.startup.runtime implementation libs.android.joda + testImplementation libs.turbine testImplementation libs.junit testImplementation libs.androidx.core @@ -274,4 +276,5 @@ dependencies { testImplementation libs.mockk testImplementation libs.kotlinx.coroutines.test testImplementation(libs.apollo.testing.support) + } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 81551ef2..d5b8980f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ + ? = null, - ) { - val bundle = params?.let { createBundle(it) } + /** + * Logs an event with the given name and parameters to Firebase Analytics. + * + * @param event The name of the event to log. + * @param params A map of parameters to include with the event (optional). + */ + fun logEvent( + event: String, + params: Map? = null, + ) { + val bundle = params?.let { createBundle(it) } - firebaseAnalytics.logEvent(event.removeSpecialCharacters(), bundle) - } +// firebaseAnalytics.logEvent(event.removeSpecialCharacters(), bundle) + } - /** - * Sets a user property with the given name and value in Firebase Analytics. - * - * @param name The name of the user property to set. - * @param value The value to set the user property to (optional). - */ - fun setUserProperty( - name: String, - value: String?, - ) { - firebaseAnalytics.setUserPropertyRelease(name, value) - } + /** + * Sets a user property with the given name and value in Firebase Analytics. + * + * @param name The name of the user property to set. + * @param value The value to set the user property to (optional). + */ + fun setUserProperty( + name: String, + value: String?, + ) { +// firebaseAnalytics.setUserPropertyRelease(name, value) + } - /** - * Logs the current screen to Firebase Analytics. - * - * @param context The current Context object. - * @param screenName A string representing the name of the current screen. - * @param screenClassOverride An optional string representing the name of the screen class to use in logs. If null, the class name of the Context will be used. - */ - fun logCurrentScreen(screenName: String) { - val screenDetails = mapOf(FirebaseAnalytics.Param.SCREEN_NAME to screenName) - if (BuildConfig.BUILD_TYPE == "release") { - logEvent( - FirebaseAnalytics.Event.SCREEN_VIEW, - screenDetails, - ) - } + /** + * Logs the current screen to Firebase Analytics. + * + * @param context The current Context object. + * @param screenName A string representing the name of the current screen. + * @param screenClassOverride An optional string representing the name of the screen class to use in logs. If null, the class name of the Context will be used. + */ + fun logCurrentScreen(screenName: String) { + val screenDetails = mapOf(FirebaseAnalytics.Param.SCREEN_NAME to screenName) + if (BuildConfig.BUILD_TYPE == "release") { + logEvent( + FirebaseAnalytics.Event.SCREEN_VIEW, + screenDetails, + ) } + } - /** - * Creates a Bundle from the given map of parameters. - * - * @param params A map of parameters to include in the Bundle. - * @return A Bundle containing the given parameters. - */ - private fun createBundle(params: Map): Bundle { - val bundle = Bundle() - params.forEach { (key, value) -> - when (value) { - is String -> bundle.putString(key, value) - is Boolean -> bundle.putBoolean(key, value) - is Int -> bundle.putInt(key, value) - is Long -> bundle.putLong(key, value) - is Double -> bundle.putDouble(key, value) - is Float -> bundle.putFloat(key, value) - } + /** + * Creates a Bundle from the given map of parameters. + * + * @param params A map of parameters to include in the Bundle. + * @return A Bundle containing the given parameters. + */ + private fun createBundle(params: Map): Bundle { + val bundle = Bundle() + params.forEach { (key, value) -> + when (value) { + is String -> bundle.putString(key, value) + is Boolean -> bundle.putBoolean(key, value) + is Int -> bundle.putInt(key, value) + is Long -> bundle.putLong(key, value) + is Double -> bundle.putDouble(key, value) + is Float -> bundle.putFloat(key, value) } - return bundle } + return bundle } +} private fun FirebaseAnalytics.setUserPropertyRelease( name: String, diff --git a/app/src/main/java/com/kl3jvi/animity/application/AnimityApplication.kt b/app/src/main/java/com/kl3jvi/animity/application/AnimityApplication.kt index 49493cc5..ae19d538 100644 --- a/app/src/main/java/com/kl3jvi/animity/application/AnimityApplication.kt +++ b/app/src/main/java/com/kl3jvi/animity/application/AnimityApplication.kt @@ -7,6 +7,7 @@ import android.content.Context import android.os.Build import android.util.Log import androidx.appcompat.app.AppCompatDelegate +import androidx.core.util.Consumer import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import androidx.work.Constraints @@ -49,7 +50,7 @@ class AnimityApplication : Application(), Configuration.Provider { } private fun initOneSignal() { - FirebaseApp.initializeApp(this) +// FirebaseApp.initializeApp(this) // OneSignal.initWithContext(this) // OneSignal.setLogLevel(OneSignal.LOG_LEVEL.NONE, OneSignal.LOG_LEVEL.NONE) // OneSignal.setAppId(Secrets.oneSignalKey) @@ -115,7 +116,12 @@ class AnimityApplication : Application(), Configuration.Provider { override fun getWorkManagerConfiguration() = Configuration.Builder() .setWorkerFactory(workerFactory) - .setMinimumLoggingLevel(Log.DEBUG) + .setInitializationExceptionHandler( + Consumer { throwable -> + Log.e("WorkManager", "Initialization error: ", throwable) + } + ) + .setMinimumLoggingLevel(Log.INFO) .build() companion object { diff --git a/app/src/main/java/com/kl3jvi/animity/data/downloader/Downloader.kt b/app/src/main/java/com/kl3jvi/animity/data/downloader/Downloader.kt index a7a18680..e07e648a 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/downloader/Downloader.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/downloader/Downloader.kt @@ -4,9 +4,6 @@ package com.kl3jvi.animity.data.downloader import android.content.Context import android.net.Uri -import android.util.Log -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.DownloadManager import com.google.android.exoplayer2.offline.DownloadRequest import com.google.android.exoplayer2.offline.DownloadService @@ -61,11 +58,6 @@ constructor( private val appContext: Context, private val downloadManager: DownloadManager, ) { - init { - getDownloadedVideos().forEach { - Log.e("Downloader", "Downloaded video: ${it.mediaId}") - } - } fun downloadVideoUrl(url: String, downloadManagerListener: DownloadManager.Listener) { DownloadService.sendAddDownload( @@ -80,18 +72,4 @@ constructor( ) downloadManager.addListener(downloadManagerListener) } - - fun getDownloadedVideos(): List { - val downloads = downloadManager.currentDownloads - return downloads.mapNotNull { download -> - if (download.state == Download.STATE_COMPLETED) { - MediaItem.Builder() - .setUri(download.request.uri) - .setMediaId(download.request.id) - .build() - } else { - null - } - } - } } diff --git a/app/src/main/java/com/kl3jvi/animity/data/enums/DnsTypes.kt b/app/src/main/java/com/kl3jvi/animity/data/enums/DnsTypes.kt index 4d2f8df1..ef33072f 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/enums/DnsTypes.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/enums/DnsTypes.kt @@ -7,28 +7,28 @@ enum class DnsTypes( GOOGLE_DNS( url = "https://dns.google/dns-query", ipAddresses = - listOf( - "8.8.4.4", - "8.8.8.8", - ), + listOf( + "8.8.4.4", + "8.8.8.8", + ), ), CLOUD_FLARE_DNS( url = "https://cloudflare-dns.com/dns-query", ipAddresses = - listOf( - "1.1.1.1", - "1.0.0.1", - "2606:4700:4700::1111", - "2606:4700:4700::1001", - ), + listOf( + "1.1.1.1", + "1.0.0.1", + "2606:4700:4700::1111", + "2606:4700:4700::1001", + ), ), AD_GUARD_DNS( url = "https://dns.adguard.com/dns-query", ipAddresses = - listOf( - "94.140.14.140", - "94.140.14.141", - ), + listOf( + "94.140.14.140", + "94.140.14.141", + ), ), ; diff --git a/app/src/main/java/com/kl3jvi/animity/data/mapper/FollowersDataQueryConverter.kt b/app/src/main/java/com/kl3jvi/animity/data/mapper/FollowersDataQueryConverter.kt index ae888aaf..bf2538b9 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/mapper/FollowersDataQueryConverter.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/mapper/FollowersDataQueryConverter.kt @@ -13,9 +13,9 @@ fun ApolloResponse.convert(): List, id = it.id, name = it.name, avatar = - UserAvatar( - large = it.avatar?.large.orEmpty(), - ), + UserAvatar( + large = it.avatar?.large.orEmpty(), + ), ) } } ?: emptyList() @@ -27,9 +27,9 @@ fun ApolloResponse.convert(): List, id = it.id, name = it.name, avatar = - UserAvatar( - large = it.avatar?.large.orEmpty(), - ), + UserAvatar( + large = it.avatar?.large.orEmpty(), + ), ) } } ?: emptyList() diff --git a/app/src/main/java/com/kl3jvi/animity/data/mapper/HomeDataQueryConverter.kt b/app/src/main/java/com/kl3jvi/animity/data/mapper/HomeDataQueryConverter.kt index d12da778..22753449 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/mapper/HomeDataQueryConverter.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/mapper/HomeDataQueryConverter.kt @@ -17,29 +17,29 @@ import com.kl3jvi.animity.fragment.HomeMedia fun ApolloResponse.convert(): HomeData { return HomeData( trendingAnime = - this.data - ?.trendingAnime - ?.media - ?.mapNotNull { it?.homeMedia?.convert() } - ?: emptyList(), + this.data + ?.trendingAnime + ?.media + ?.mapNotNull { it?.homeMedia?.convert() } + ?: emptyList(), popularAnime = - this.data - ?.popularAnime - ?.media - ?.mapNotNull { it?.homeMedia?.convert() } - ?: emptyList(), + this.data + ?.popularAnime + ?.media + ?.mapNotNull { it?.homeMedia?.convert() } + ?: emptyList(), movies = - this.data - ?.movies - ?.media - ?.mapNotNull { it?.homeMedia?.convert() } - ?: emptyList(), + this.data + ?.movies + ?.media + ?.mapNotNull { it?.homeMedia?.convert() } + ?: emptyList(), review = - this.data - ?.review - ?.reviews - ?.mapNotNull { it.convert() } - ?: emptyList(), + this.data + ?.review + ?.reviews + ?.mapNotNull { it.convert() } + ?: emptyList(), ) } @@ -55,25 +55,25 @@ fun HomeDataQuery.Review1?.convert(): Review { ratingAmount = this?.ratingAmount ?: 0, score = this?.score ?: 0, user = - User( - id = this?.user?.id ?: 0, - name = this?.user?.name.orEmpty(), - avatar = - UserAvatar( - this?.user?.avatar?.large.orEmpty(), - this?.user?.avatar?.medium.orEmpty(), - ), + User( + id = this?.user?.id ?: 0, + name = this?.user?.name.orEmpty(), + avatar = + UserAvatar( + this?.user?.avatar?.large.orEmpty(), + this?.user?.avatar?.medium.orEmpty(), ), + ), aniListMedia = - AniListMedia( - idAniList = this?.media?.homeMedia?.id ?: 0, - title = MediaTitle(userPreferred = this?.media?.homeMedia?.title?.userPreferred.orEmpty()), - bannerImage = this?.media?.homeMedia?.bannerImage.orEmpty(), - coverImage = - MediaCoverImage( - large = this?.media?.homeMedia?.coverImage?.large.orEmpty(), - ), + AniListMedia( + idAniList = this?.media?.homeMedia?.id ?: 0, + title = MediaTitle(userPreferred = this?.media?.homeMedia?.title?.userPreferred.orEmpty()), + bannerImage = this?.media?.homeMedia?.bannerImage.orEmpty(), + coverImage = + MediaCoverImage( + large = this?.media?.homeMedia?.coverImage?.large.orEmpty(), ), + ), ) } @@ -90,17 +90,17 @@ fun HomeMedia?.convert(): AniListMedia { status = this?.status, description = this?.description.orEmpty(), startDate = - if (this?.startDate?.year != null) { - FuzzyDate(this.startDate.year, this.startDate.month, this.startDate.day) - } else { - null - }, + if (this?.startDate?.year != null) { + FuzzyDate(this.startDate.year, this.startDate.month, this.startDate.day) + } else { + null + }, coverImage = - MediaCoverImage( - this?.coverImage?.extraLarge.orEmpty(), - this?.coverImage?.large.orEmpty(), - this?.coverImage?.medium.orEmpty(), - ), + MediaCoverImage( + this?.coverImage?.extraLarge.orEmpty(), + this?.coverImage?.large.orEmpty(), + this?.coverImage?.medium.orEmpty(), + ), bannerImage = this?.bannerImage.orEmpty(), genres = this?.genres?.mapNotNull { Genre(name = it.orEmpty()) } ?: emptyList(), averageScore = this?.averageScore ?: 0, @@ -109,6 +109,9 @@ fun HomeMedia?.convert(): AniListMedia { ) } +private fun sampleEmptyFuntion() { +} + fun HomeMedia.StreamingEpisode?.convert() = Episodes( this?.title, diff --git a/app/src/main/java/com/kl3jvi/animity/data/mapper/MessageConverter.kt b/app/src/main/java/com/kl3jvi/animity/data/mapper/MessageConverter.kt index 23989452..fc515409 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/mapper/MessageConverter.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/mapper/MessageConverter.kt @@ -23,19 +23,19 @@ fun ApolloResponse.convert( message = sentReply.text ?: throw Exception("Error sending message: message is null"), createdAt = sentReply.createdAt, recipient = - Message.User( - id = recipientId, - name = recipientName, - avatarLarge = recipientAvatarLarge, - avatarMedium = recipientAvatarMedium, - ), + Message.User( + id = recipientId, + name = recipientName, + avatarLarge = recipientAvatarLarge, + avatarMedium = recipientAvatarMedium, + ), messenger = - Message.User( - id = user?.id ?: throw Exception("Error sending message: user ID is null"), - name = user.name, - avatarLarge = user.avatar?.large ?: "", - avatarMedium = user.avatar?.medium ?: "", - ), + Message.User( + id = user?.id ?: throw Exception("Error sending message: user ID is null"), + name = user.name, + avatarLarge = user.avatar?.large ?: "", + avatarMedium = user.avatar?.medium ?: "", + ), parentId = null, ) } else if (sentMessage != null) { @@ -45,28 +45,28 @@ fun ApolloResponse.convert( Message( id = sentMessage.id, senderUserId = - messenger?.id - ?: throw Exception("Error sending message: messenger ID is null"), + messenger?.id + ?: throw Exception("Error sending message: messenger ID is null"), message = - sentMessage.message - ?: throw Exception("Error sending message: message is null"), + sentMessage.message + ?: throw Exception("Error sending message: message is null"), createdAt = sentMessage.createdAt, recipient = - Message.User( - id = - recipient?.id - ?: throw Exception("Error sending message: recipient ID is null"), - name = recipient.name, - avatarLarge = recipient.avatar?.large ?: "", - avatarMedium = recipient.avatar?.medium ?: "", - ), + Message.User( + id = + recipient?.id + ?: throw Exception("Error sending message: recipient ID is null"), + name = recipient.name, + avatarLarge = recipient.avatar?.large ?: "", + avatarMedium = recipient.avatar?.medium ?: "", + ), messenger = - Message.User( - id = messenger.id, - name = messenger.name, - avatarLarge = messenger.avatar?.large ?: "", - avatarMedium = messenger.avatar?.medium ?: "", - ), + Message.User( + id = messenger.id, + name = messenger.name, + avatarLarge = messenger.avatar?.large ?: "", + avatarMedium = messenger.avatar?.medium ?: "", + ), parentId = null, ) } else { @@ -88,41 +88,41 @@ fun ApolloResponse.convert(): List { message = messageActivity?.onMessageActivity?.message ?: "", createdAt = messageActivity?.onMessageActivity?.createdAt.or1(), recipient = - Message.User( - id = recipient?.id ?: 0, - name = recipient?.name ?: "", - avatarLarge = recipient?.avatar?.large ?: "", - avatarMedium = recipient?.avatar?.medium ?: "", - ), + Message.User( + id = recipient?.id ?: 0, + name = recipient?.name ?: "", + avatarLarge = recipient?.avatar?.large ?: "", + avatarMedium = recipient?.avatar?.medium ?: "", + ), messenger = - Message.User( - id = messenger?.id ?: 0, - name = messenger?.name ?: "", - avatarLarge = messenger?.avatar?.large ?: "", - avatarMedium = messenger?.avatar?.medium ?: "", - ), + Message.User( + id = messenger?.id ?: 0, + name = messenger?.name ?: "", + avatarLarge = messenger?.avatar?.large ?: "", + avatarMedium = messenger?.avatar?.medium ?: "", + ), replies = emptyList(), ) baseMessage.copy( replies = - messageActivity?.onMessageActivity?.replies?.mapNotNull { reply -> - if (reply == null) { - null - } else { - Message.Reply( - id = reply.id, - message = reply.text ?: "", - createdAt = reply.createdAt, - user = - Message.User( - id = reply.user?.id ?: 0, - name = reply.user?.name ?: "", - avatarLarge = reply.user?.avatar?.large ?: "", - avatarMedium = reply.user?.avatar?.medium ?: "", - ), - ) - } - } ?: emptyList(), + messageActivity?.onMessageActivity?.replies?.mapNotNull { reply -> + if (reply == null) { + null + } else { + Message.Reply( + id = reply.id, + message = reply.text ?: "", + createdAt = reply.createdAt, + user = + Message.User( + id = reply.user?.id ?: 0, + name = reply.user?.name ?: "", + avatarLarge = reply.user?.avatar?.large ?: "", + avatarMedium = reply.user?.avatar?.medium ?: "", + ), + ) + } + } ?: emptyList(), ) } } diff --git a/app/src/main/java/com/kl3jvi/animity/data/mapper/NotificationsQueryDataConverter.kt b/app/src/main/java/com/kl3jvi/animity/data/mapper/NotificationsQueryDataConverter.kt index 392f87dc..95b4c60b 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/mapper/NotificationsQueryDataConverter.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/mapper/NotificationsQueryDataConverter.kt @@ -79,15 +79,15 @@ private fun NotificationsQuery.OnThreadCommentReplyNotification.toNotification() episode = null, createdAt = createdAt, user = - User( - id = this.user?.id ?: 0, - name = this.user?.name.orEmpty(), - avatar = - UserAvatar( - this.user?.avatar?.large.orEmpty(), - this.user?.avatar?.medium.orEmpty(), - ), + User( + id = this.user?.id ?: 0, + name = this.user?.name.orEmpty(), + avatar = + UserAvatar( + this.user?.avatar?.large.orEmpty(), + this.user?.avatar?.medium.orEmpty(), ), + ), contexts = listOf(this.context), type = NotificationType.Threads, ) @@ -99,75 +99,75 @@ private fun NotificationsQuery.OnThreadCommentMentionNotification.toNotification episode = null, contexts = listOf(this.context), user = - User( - id = this.user?.id ?: 0, - name = this.user?.name.orEmpty(), - avatar = - UserAvatar( - this.user?.avatar?.large.orEmpty(), - this.user?.avatar?.medium.orEmpty(), - ), + User( + id = this.user?.id ?: 0, + name = this.user?.name.orEmpty(), + avatar = + UserAvatar( + this.user?.avatar?.large.orEmpty(), + this.user?.avatar?.medium.orEmpty(), ), + ), createdAt = createdAt, ) private fun NotificationsQuery.OnActivityMentionNotification.toNotification() = Notification( type = - NotificationType.Activity( - User( - id = this.user?.id ?: 0, - name = this.user?.name.orEmpty(), - avatar = - UserAvatar( - this.user?.avatar?.large.orEmpty(), - this.user?.avatar?.medium.orEmpty(), - ), + NotificationType.Activity( + User( + id = this.user?.id ?: 0, + name = this.user?.name.orEmpty(), + avatar = + UserAvatar( + this.user?.avatar?.large.orEmpty(), + this.user?.avatar?.medium.orEmpty(), ), ), + ), id = this.id, episode = null, contexts = listOf(this.context), user = - User( - id = this.user?.id ?: 0, - name = this.user?.name.orEmpty(), - avatar = - UserAvatar( - this.user?.avatar?.large.orEmpty(), - this.user?.avatar?.medium.orEmpty(), - ), + User( + id = this.user?.id ?: 0, + name = this.user?.name.orEmpty(), + avatar = + UserAvatar( + this.user?.avatar?.large.orEmpty(), + this.user?.avatar?.medium.orEmpty(), ), + ), createdAt = createdAt, ) private fun NotificationsQuery.OnActivityReplyNotification.toNotification() = Notification( type = - NotificationType.Activity( - User( - id = this.user?.id ?: 0, - name = this.user?.name.orEmpty(), - avatar = - UserAvatar( - this.user?.avatar?.large.orEmpty(), - this.user?.avatar?.medium.orEmpty(), - ), + NotificationType.Activity( + User( + id = this.user?.id ?: 0, + name = this.user?.name.orEmpty(), + avatar = + UserAvatar( + this.user?.avatar?.large.orEmpty(), + this.user?.avatar?.medium.orEmpty(), ), ), + ), id = this.id, episode = null, contexts = listOf(this.context), user = - User( - id = this.user?.id ?: 0, - name = this.user?.name.orEmpty(), - avatar = - UserAvatar( - this.user?.avatar?.large.orEmpty(), - this.user?.avatar?.medium.orEmpty(), - ), + User( + id = this.user?.id ?: 0, + name = this.user?.name.orEmpty(), + avatar = + UserAvatar( + this.user?.avatar?.large.orEmpty(), + this.user?.avatar?.medium.orEmpty(), ), + ), createdAt = createdAt, ) @@ -184,90 +184,90 @@ private fun NotificationsQuery.OnAiringNotification.toNotification() = private fun NotificationsQuery.OnFollowingNotification.toNotification() = Notification( type = - NotificationType.Following( - User( - id = this.user?.id ?: 0, - name = this.user?.name.orEmpty(), - avatar = - UserAvatar( - this.user?.avatar?.large.orEmpty(), - this.user?.avatar?.medium.orEmpty(), - ), + NotificationType.Following( + User( + id = this.user?.id ?: 0, + name = this.user?.name.orEmpty(), + avatar = + UserAvatar( + this.user?.avatar?.large.orEmpty(), + this.user?.avatar?.medium.orEmpty(), ), ), + ), id = this.id, episode = null, contexts = listOf(this.context), user = - User( - id = this.user?.id ?: 0, - name = this.user?.name.orEmpty(), - avatar = - UserAvatar( - this.user?.avatar?.large.orEmpty(), - this.user?.avatar?.medium.orEmpty(), - ), + User( + id = this.user?.id ?: 0, + name = this.user?.name.orEmpty(), + avatar = + UserAvatar( + this.user?.avatar?.large.orEmpty(), + this.user?.avatar?.medium.orEmpty(), ), + ), createdAt = createdAt, ) private fun NotificationsQuery.OnActivityLikeNotification.toNotification() = Notification( type = - NotificationType.Activity( - User( - id = this.user?.id ?: 0, - name = this.user?.name.orEmpty(), - avatar = - UserAvatar( - this.user?.avatar?.large.orEmpty(), - this.user?.avatar?.medium.orEmpty(), - ), + NotificationType.Activity( + User( + id = this.user?.id ?: 0, + name = this.user?.name.orEmpty(), + avatar = + UserAvatar( + this.user?.avatar?.large.orEmpty(), + this.user?.avatar?.medium.orEmpty(), ), ), + ), id = this.id, episode = null, contexts = listOf(this.context), user = - User( - id = this.user?.id ?: 0, - name = this.user?.name.orEmpty(), - avatar = - UserAvatar( - this.user?.avatar?.large.orEmpty(), - this.user?.avatar?.medium.orEmpty(), - ), + User( + id = this.user?.id ?: 0, + name = this.user?.name.orEmpty(), + avatar = + UserAvatar( + this.user?.avatar?.large.orEmpty(), + this.user?.avatar?.medium.orEmpty(), ), + ), createdAt = createdAt, ) private fun NotificationsQuery.OnActivityMessageNotification.toNotification() = Notification( type = - NotificationType.Activity( - User( - id = this.user?.id ?: 0, - name = this.user?.name.orEmpty(), - avatar = - UserAvatar( - this.user?.avatar?.large.orEmpty(), - this.user?.avatar?.medium.orEmpty(), - ), + NotificationType.Activity( + User( + id = this.user?.id ?: 0, + name = this.user?.name.orEmpty(), + avatar = + UserAvatar( + this.user?.avatar?.large.orEmpty(), + this.user?.avatar?.medium.orEmpty(), ), ), + ), id = this.id, episode = null, contexts = listOf(this.context), user = - User( - id = this.user?.id ?: 0, - name = this.user?.name.orEmpty(), - avatar = - UserAvatar( - this.user?.avatar?.large.orEmpty(), - this.user?.avatar?.medium.orEmpty(), - ), + User( + id = this.user?.id ?: 0, + name = this.user?.name.orEmpty(), + avatar = + UserAvatar( + this.user?.avatar?.large.orEmpty(), + this.user?.avatar?.medium.orEmpty(), ), + ), createdAt = createdAt, ) @@ -284,21 +284,21 @@ private fun NotificationsQuery.Media?.convert(): AniListMedia { status = this?.homeMedia?.status, description = this?.homeMedia?.description.orEmpty(), startDate = - if (this?.homeMedia?.startDate?.year != null) { - FuzzyDate( - this.homeMedia.startDate.year, - this.homeMedia.startDate.month, - this.homeMedia.startDate.day, - ) - } else { - null - }, + if (this?.homeMedia?.startDate?.year != null) { + FuzzyDate( + this.homeMedia.startDate.year, + this.homeMedia.startDate.month, + this.homeMedia.startDate.day, + ) + } else { + null + }, coverImage = - MediaCoverImage( - this?.homeMedia?.coverImage?.extraLarge.orEmpty(), - this?.homeMedia?.coverImage?.large.orEmpty(), - this?.homeMedia?.coverImage?.medium.orEmpty(), - ), + MediaCoverImage( + this?.homeMedia?.coverImage?.extraLarge.orEmpty(), + this?.homeMedia?.coverImage?.large.orEmpty(), + this?.homeMedia?.coverImage?.medium.orEmpty(), + ), bannerImage = this?.homeMedia?.bannerImage.orEmpty(), genres = this?.homeMedia?.genres?.mapNotNull { Genre(name = it.orEmpty()) } ?: emptyList(), averageScore = this?.homeMedia?.averageScore ?: 0, diff --git a/app/src/main/java/com/kl3jvi/animity/data/model/ui_models/EpisodeModel.kt b/app/src/main/java/com/kl3jvi/animity/data/model/ui_models/EpisodeModel.kt index d320f5f9..57ca5b06 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/model/ui_models/EpisodeModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/model/ui_models/EpisodeModel.kt @@ -14,6 +14,7 @@ data class EpisodeModel( var episodeType: String = "", var percentage: Int = 0, var isFiller: Boolean = false, + var isDownloaded: Boolean = false ) : Parcelable { fun getEpisodeNumberOnly(): Int? { return episodeNumber.split(" ").lastOrNull()?.toIntOrNull() diff --git a/app/src/main/java/com/kl3jvi/animity/data/network/anilist_service/AniListGraphQlClient.kt b/app/src/main/java/com/kl3jvi/animity/data/network/anilist_service/AniListGraphQlClient.kt index 47554ead..80e5e164 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/network/anilist_service/AniListGraphQlClient.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/network/anilist_service/AniListGraphQlClient.kt @@ -44,103 +44,103 @@ class AniListGraphQlClient apolloClient.query(UserQuery(Optional.presentIfNotNull(userId))).execute() } - override suspend fun fetchUsers( - query: String, - page: Int, - ) = performance.measureAndTrace("fetchUsers") { - apolloClient.query( - SearchUsersQuery( - Optional.presentIfNotNull(query), - Optional.presentIfNotNull(page), - ), - ).execute() - } - - override suspend fun getUserData(id: Int?) = - performance.measureAndTrace("getUserData") { - apolloClient.query(UserQuery(Optional.Present(id))).execute() - } - - override suspend fun getNotifications(page: Int) = - performance.measureAndTrace("getNotifications") { - apolloClient.query(NotificationsQuery(Optional.Present(page))).execute() - } - - override suspend fun getFollowersAndFollowing(page: Int) = - performance.measureAndTrace("getFollowersAndFollowing") { - apolloClient.query(GetFollowersListQuery(page)).execute() - } - - // 2. Queries related to anime media: + override suspend fun fetchUsers( + query: String, + page: Int, + ) = performance.measureAndTrace("fetchUsers") { + apolloClient.query( + SearchUsersQuery( + Optional.presentIfNotNull(query), + Optional.presentIfNotNull(page), + ), + ).execute() + } - override suspend fun getAnimeListData(userId: Int?) = - performance.measureAndTrace("getAnimeListData") { - apolloClient.query(AnimeListCollectionQuery(Optional.presentIfNotNull(userId))) - .execute() - } + override suspend fun getUserData(id: Int?) = + performance.measureAndTrace("getUserData") { + apolloClient.query(UserQuery(Optional.Present(id))).execute() + } - override suspend fun fetchSearchAniListData( - query: String, - page: Int, - toMediaSort: List, - ) = performance.measureAndTrace("fetchSearchAniListData") { - apolloClient.query( - SearchAnimeQuery( - Optional.presentIfNotNull(query), - Optional.presentIfNotNull(page), - Optional.present(toMediaSort), - ), - ).execute() + override suspend fun getNotifications(page: Int) = + performance.measureAndTrace("getNotifications") { + apolloClient.query(NotificationsQuery(Optional.Present(page))).execute() } - override suspend fun getFavoriteAnimes( - userId: Int?, - page: Int?, - ) = performance.measureAndTrace("getFavoriteAnimes") { - apolloClient.query( - FavoritesAnimeQuery( - Optional.Present(userId), - Optional.Present(page), - ), - ).execute() + override suspend fun getFollowersAndFollowing(page: Int) = + performance.measureAndTrace("getFollowersAndFollowing") { + apolloClient.query(GetFollowersListQuery(page)).execute() } - override suspend fun getTopTenTrending() = - performance.measureAndTrace("getTopTenTrending") { - apolloClient.query(TrendingMediaQuery()).execute() - } + // 2. Queries related to anime media: - override suspend fun getAiringAnimeForDate( - startDate: Int?, - endDate: Int?, - ) = performance.measureAndTrace("getAiringAnimeForDate") { - apolloClient.query(AiringQuery(Optional.present(startDate), Optional.present(endDate))) + override suspend fun getAnimeListData(userId: Int?) = + performance.measureAndTrace("getAnimeListData") { + apolloClient.query(AnimeListCollectionQuery(Optional.presentIfNotNull(userId))) .execute() } - // 3. Mutation related operations: + override suspend fun fetchSearchAniListData( + query: String, + page: Int, + toMediaSort: List, + ) = performance.measureAndTrace("fetchSearchAniListData") { + apolloClient.query( + SearchAnimeQuery( + Optional.presentIfNotNull(query), + Optional.presentIfNotNull(page), + Optional.present(toMediaSort), + ), + ).execute() + } - override suspend fun markAnimeAsFavorite(animeId: Int?) = - performance.measureAndTrace("markAnimeAsFavorite") { - apolloClient.mutation(ToggleFavouriteMutation(Optional.Present(animeId))).execute() - } + override suspend fun getFavoriteAnimes( + userId: Int?, + page: Int?, + ) = performance.measureAndTrace("getFavoriteAnimes") { + apolloClient.query( + FavoritesAnimeQuery( + Optional.Present(userId), + Optional.Present(page), + ), + ).execute() + } - override suspend fun markAnimeStatus( - mediaId: Int, - status: MediaListStatus, - ) = performance.measureAndTrace("markAnimeStatus") { - apolloClient.mutation(SaveMediaMutation(mediaId, status)).execute() + override suspend fun getTopTenTrending() = + performance.measureAndTrace("getTopTenTrending") { + apolloClient.query(TrendingMediaQuery()).execute() } - override suspend fun markWatchedEpisode( - mediaId: Int, - episodesWatched: Int, - ) = performance.measureAndTrace("markWatchedEpisode") { - apolloClient.mutation(SaveMediaListEntryMutation(mediaId, episodesWatched)).execute() + override suspend fun getAiringAnimeForDate( + startDate: Int?, + endDate: Int?, + ) = performance.measureAndTrace("getAiringAnimeForDate") { + apolloClient.query(AiringQuery(Optional.present(startDate), Optional.present(endDate))) + .execute() + } + + // 3. Mutation related operations: + + override suspend fun markAnimeAsFavorite(animeId: Int?) = + performance.measureAndTrace("markAnimeAsFavorite") { + apolloClient.mutation(ToggleFavouriteMutation(Optional.Present(animeId))).execute() } - override suspend fun followUser(id: Int) = - performance.measureAndTrace("followUser") { - apolloClient.mutation(ToggleFollowUserMutation(Optional.present(id))).execute() - } + override suspend fun markAnimeStatus( + mediaId: Int, + status: MediaListStatus, + ) = performance.measureAndTrace("markAnimeStatus") { + apolloClient.mutation(SaveMediaMutation(mediaId, status)).execute() + } + + override suspend fun markWatchedEpisode( + mediaId: Int, + episodesWatched: Int, + ) = performance.measureAndTrace("markWatchedEpisode") { + apolloClient.mutation(SaveMediaListEntryMutation(mediaId, episodesWatched)).execute() } + + override suspend fun followUser(id: Int) = + performance.measureAndTrace("followUser") { + apolloClient.mutation(ToggleFollowUserMutation(Optional.present(id))).execute() + } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/network/anilist_service/AuthClient.kt b/app/src/main/java/com/kl3jvi/animity/data/network/anilist_service/AuthClient.kt index a3337ef3..0712376e 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/network/anilist_service/AuthClient.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/network/anilist_service/AuthClient.kt @@ -6,53 +6,53 @@ import com.kl3jvi.animity.data.model.auth_models.RefreshTokenRequest import javax.inject.Inject class AuthClient - @Inject - constructor( - private val aniListService: AniListAuthService, - ) : Authenticator { - /** - * Retrieves an access token from the AniList API. - * - * @param grantType The type of grant to use for the request. - * @param clientId The client ID for the AniList API. - * @param clientSecret The client secret for the AniList API. - * @param redirectUri The redirect URI for the AniList API. - * @param code The authorization code to exchange for an access token. - * @return A [Result] object containing the [AuthResponse] on success, or the exception on failure. - */ - override suspend fun getAccessToken( - grantType: String, - clientId: Int, - clientSecret: String, - redirectUri: String, - code: String, - ): Result { - return runCatching { - aniListService.getAccessToken( - AniListAuth( - grant_type = grantType, - client_id = clientId, - client_secret = clientSecret, - redirect_uri = redirectUri, - code = code, - ), - ) - } +@Inject +constructor( + private val aniListService: AniListAuthService, +) : Authenticator { + /** + * Retrieves an access token from the AniList API. + * + * @param grantType The type of grant to use for the request. + * @param clientId The client ID for the AniList API. + * @param clientSecret The client secret for the AniList API. + * @param redirectUri The redirect URI for the AniList API. + * @param code The authorization code to exchange for an access token. + * @return A [Result] object containing the [AuthResponse] on success, or the exception on failure. + */ + override suspend fun getAccessToken( + grantType: String, + clientId: Int, + clientSecret: String, + redirectUri: String, + code: String, + ): Result { + return runCatching { + aniListService.getAccessToken( + AniListAuth( + grant_type = grantType, + client_id = clientId, + client_secret = clientSecret, + redirect_uri = redirectUri, + code = code, + ), + ) } + } - override suspend fun refreshToken( - clientId: Int, - clientSecret: String, - refreshToken: String, - ): Result { - return runCatching { - aniListService.refreshToken( - RefreshTokenRequest( - clientId = clientId, - clientSecret = clientSecret, - refreshToken = refreshToken, - ), - ) - } + override suspend fun refreshToken( + clientId: Int, + clientSecret: String, + refreshToken: String, + ): Result { + return runCatching { + aniListService.refreshToken( + RefreshTokenRequest( + clientId = clientId, + clientSecret = clientSecret, + refreshToken = refreshToken, + ), + ) } } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/base/ApiServiceSingleton.kt b/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/base/ApiServiceSingleton.kt index 921f9ec9..15236194 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/base/ApiServiceSingleton.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/base/ApiServiceSingleton.kt @@ -1,12 +1,12 @@ package com.kl3jvi.animity.data.network.anime_service.base import com.kl3jvi.animity.settings.Settings -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory import javax.inject.Inject import javax.inject.Provider import javax.inject.Singleton +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory /** * A singleton class that provides an instance of the API service using Retrofit library. @@ -15,41 +15,41 @@ import javax.inject.Singleton */ @Singleton class ApiServiceSingleton - @Inject - constructor( - baseUrlProvider: Provider, - private val okHttpClient: OkHttpClient, - private val settings: Settings, - ) { - private var retrofit: Retrofit = createRetrofit(baseUrlProvider.get()) +@Inject +constructor( + baseUrlProvider: Provider, + private val okHttpClient: OkHttpClient, + private val settings: Settings, +) { + private var retrofit: Retrofit = createRetrofit(baseUrlProvider.get()) - /** - * Returns an instance of the API service interface for the given class type. - * @param clazz Class of the API service interface. - * @return An instance of the API service interface. - */ - fun getApiService(clazz: Class): T { - return retrofit.create(clazz) - } + /** + * Returns an instance of the API service interface for the given class type. + * @param clazz Class of the API service interface. + * @return An instance of the API service interface. + */ + fun getApiService(clazz: Class): T { + return retrofit.create(clazz) + } - /** - * Updates the base URL of the API and creates a new instance of Retrofit with the updated URL. - * @param newBaseUrl The new base URL of the API. - */ - fun updateBaseUrl(newBaseUrl: String) { - retrofit = createRetrofit(newBaseUrl) - } + /** + * Updates the base URL of the API and creates a new instance of Retrofit with the updated URL. + * @param newBaseUrl The new base URL of the API. + */ + fun updateBaseUrl(newBaseUrl: String) { + retrofit = createRetrofit(newBaseUrl) + } - /** - * Creates a new instance of Retrofit with the given base URL and OkHttpClient. - * @param baseUrl The base URL of the API. - * @return A new instance of Retrofit. - */ - private fun createRetrofit(baseUrl: String): Retrofit { - return Retrofit.Builder() - .baseUrl(baseUrl) - .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create()) - .build() - } + /** + * Creates a new instance of Retrofit with the given base URL and OkHttpClient. + * @param baseUrl The base URL of the API. + * @return A new instance of Retrofit. + */ + private fun createRetrofit(baseUrl: String): Retrofit { + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/bili/BiliAnimeApiClient.kt b/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/bili/BiliAnimeApiClient.kt index 26f8af83..1eb71f6f 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/bili/BiliAnimeApiClient.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/bili/BiliAnimeApiClient.kt @@ -8,33 +8,33 @@ import com.kl3jvi.animity.utils.Constants import javax.inject.Inject class BiliAnimeApiClient - @Inject - constructor( - apiServiceSingleton: ApiServiceSingleton, - override val parser: AniWaveParser, - ) : BaseClient { - override var animeService: BaseService = - apiServiceSingleton.run { - updateBaseUrl(Constants.BILI_URL) - getApiService(BiliAnimeService::class.java) - } - - override suspend fun fetchEpisodeMediaUrl( - header: Map, - episodeUrl: String, - extra: List, - ): T { - TODO("Not yet implemented") +@Inject +constructor( + apiServiceSingleton: ApiServiceSingleton, + override val parser: AniWaveParser, +) : BaseClient { + override var animeService: BaseService = + apiServiceSingleton.run { + updateBaseUrl(Constants.BILI_URL) + getApiService(BiliAnimeService::class.java) } - override suspend fun fetchEpisodeList( - episodeUrl: String, - extra: List, - ): T { - TODO("Not yet implemented") - } + override suspend fun fetchEpisodeMediaUrl( + header: Map, + episodeUrl: String, + extra: List, + ): T { + TODO("Not yet implemented") + } - override suspend fun getEpisodeTitles(id: Int): T { - TODO("Not yet implemented") - } + override suspend fun fetchEpisodeList( + episodeUrl: String, + extra: List, + ): T { + TODO("Not yet implemented") + } + + override suspend fun getEpisodeTitles(id: Int): T { + TODO("Not yet implemented") } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/enime/EnimeClient.kt b/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/enime/EnimeClient.kt index bdd6c351..5a615884 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/enime/EnimeClient.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/enime/EnimeClient.kt @@ -10,36 +10,36 @@ import javax.inject.Inject @Suppress("UNCHECKED_CAST") class EnimeClient - @Inject - constructor( - apiServiceSingleton: ApiServiceSingleton, - override val parser: GoGoParser, // Not used here - ) : BaseClient { - override var animeService: BaseService = - apiServiceSingleton.run { - updateBaseUrl(Constants.ENIME_BASE_URL) - getApiService(EnimeService::class.java) - } - - override suspend fun fetchEpisodeMediaUrl( - header: Map, - episodeUrl: String, - extra: List, - ): T { - val animeList = - mutableListOf().apply { - add((animeService as EnimeService).getEnimeSource(extra.firstOrNull().toString()).url) - }.toList() - return animeList as T +@Inject +constructor( + apiServiceSingleton: ApiServiceSingleton, + override val parser: GoGoParser, // Not used here +) : BaseClient { + override var animeService: BaseService = + apiServiceSingleton.run { + updateBaseUrl(Constants.ENIME_BASE_URL) + getApiService(EnimeService::class.java) } - override suspend fun fetchEpisodeList( - episodeUrl: String, - extra: List, - ): T = - (animeService as EnimeService).getEnimeEpisodesIds( - extra.first()?.toString()?.toInt().or1(), - ) as T - - override suspend fun getEpisodeTitles(id: Int): T = Unit as T + override suspend fun fetchEpisodeMediaUrl( + header: Map, + episodeUrl: String, + extra: List, + ): T { + val animeList = + mutableListOf().apply { + add((animeService as EnimeService).getEnimeSource(extra.firstOrNull().toString()).url) + }.toList() + return animeList as T } + + override suspend fun fetchEpisodeList( + episodeUrl: String, + extra: List, + ): T = + (animeService as EnimeService).getEnimeEpisodesIds( + extra.first()?.toString()?.toInt().or1(), + ) as T + + override suspend fun getEpisodeTitles(id: Int): T = Unit as T +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/gogo/GogoAnimeApiClient.kt b/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/gogo/GogoAnimeApiClient.kt index 76dff0c1..29a1ecfb 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/gogo/GogoAnimeApiClient.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/network/anime_service/gogo/GogoAnimeApiClient.kt @@ -6,134 +6,134 @@ import com.kl3jvi.animity.data.network.anime_service.base.BaseClient import com.kl3jvi.animity.data.network.anime_service.base.BaseService import com.kl3jvi.animity.parsers.GoGoParser import com.kl3jvi.animity.utils.Constants +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import javax.inject.Inject @Suppress("UNCHECKED_CAST") class GogoAnimeApiClient - @Inject - constructor( - apiServiceSingleton: ApiServiceSingleton, - override val parser: GoGoParser, - ) : BaseClient { - override var animeService: BaseService = - apiServiceSingleton.run { - updateBaseUrl(Constants.GOGO_BASE_URL) - getApiService(GogoAnimeService::class.java) - } - - private suspend fun fetchAnimeInfo( - header: Map, - episodeUrl: String, - ) = withContext(Dispatchers.IO) { - (animeService as GogoAnimeService).fetchAnimeInfo( - header, - episodeUrl, - ).string() +@Inject +constructor( + apiServiceSingleton: ApiServiceSingleton, + override val parser: GoGoParser, +) : BaseClient { + override var animeService: BaseService = + apiServiceSingleton.run { + updateBaseUrl(Constants.GOGO_BASE_URL) + getApiService(GogoAnimeService::class.java) } - override suspend fun fetchEpisodeList( - episodeUrl: String, - extra: List, - ): T { - val animeInfo = - fetchAnimeInfo( - Constants.getNetworkHeader(), - episodeUrl = episodeUrl, - ).run(parser::parseAnimeInfo) - - return (animeService as GogoAnimeService).fetchEpisodeList( - header = Constants.getNetworkHeader(), - id = animeInfo.id, - endEpisode = animeInfo.endEpisode, - alias = animeInfo.alias, - ) as T - } + private suspend fun fetchAnimeInfo( + header: Map, + episodeUrl: String, + ) = withContext(Dispatchers.IO) { + (animeService as GogoAnimeService).fetchAnimeInfo( + header, + episodeUrl, + ).string() + } + + override suspend fun fetchEpisodeList( + episodeUrl: String, + extra: List, + ): T { + val animeInfo = + fetchAnimeInfo( + Constants.getNetworkHeader(), + episodeUrl = episodeUrl, + ).run(parser::parseAnimeInfo) + + return (animeService as GogoAnimeService).fetchEpisodeList( + header = Constants.getNetworkHeader(), + id = animeInfo.id, + endEpisode = animeInfo.endEpisode, + alias = animeInfo.alias, + ) as T + } - override suspend fun getEpisodeTitles(id: Int): T = Unit as T - - override suspend fun fetchEpisodeMediaUrl( - header: Map, - episodeUrl: String, - extra: List, - ): T { - val urls = mutableListOf>() - // fetch the current episode - urls.add(getParsedUrls(header, episodeUrl)) - - // fetch and parse the current episode to get nextEpisodeUrl - val episodeInfo = - parser.parseMediaUrl( - (animeService as GogoAnimeService).fetchEpisodeMediaUrl( - header, - episodeUrl, - ).string(), - ) - - // fetch the next episode if exists - if (episodeInfo.nextEpisodeUrl != null) { - urls.add(getParsedUrls(header, episodeInfo.nextEpisodeUrl.orEmpty())) - } - - return urls as T + override suspend fun getEpisodeTitles(id: Int): T = Unit as T + + override suspend fun fetchEpisodeMediaUrl( + header: Map, + episodeUrl: String, + extra: List, + ): T { + val urls = mutableListOf>() + // fetch the current episode + urls.add(getParsedUrls(header, episodeUrl)) + + // fetch and parse the current episode to get nextEpisodeUrl + val episodeInfo = + parser.parseMediaUrl( + (animeService as GogoAnimeService).fetchEpisodeMediaUrl( + header, + episodeUrl, + ).string(), + ) + + // fetch the next episode if exists + if (episodeInfo.nextEpisodeUrl != null) { + urls.add(getParsedUrls(header, episodeInfo.nextEpisodeUrl.orEmpty())) } - private suspend fun getParsedUrls( - header: Map, - episodeUrl: String, - ): List { - val episodeInfo = - parser.parseMediaUrl( - (animeService as GogoAnimeService).fetchEpisodeMediaUrl( - header, - episodeUrl, - ).string(), - ) - - val id = - Regex("id=([^&]+)").find(episodeInfo.vidCdnUrl.orEmpty())?.value?.removePrefix("id=") - - val ajaxResponse = - parser.parseEncryptAjax( - response = - fetchM3u8Url( - header = header, - url = episodeInfo.vidCdnUrl.orEmpty(), - ).string(), - id = id.orEmpty(), - ) - - val streamUrl = "${Constants.REFERER}encrypt-ajax.php?$ajaxResponse" - - return parser.parseEncryptedUrls( - fetchM3u8PreProcessor( + return urls as T + } + + private suspend fun getParsedUrls( + header: Map, + episodeUrl: String, + ): List { + val episodeInfo = + parser.parseMediaUrl( + (animeService as GogoAnimeService).fetchEpisodeMediaUrl( + header, + episodeUrl, + ).string(), + ) + + val id = + Regex("id=([^&]+)").find(episodeInfo.vidCdnUrl.orEmpty())?.value?.removePrefix("id=") + + val ajaxResponse = + parser.parseEncryptAjax( + response = + fetchM3u8Url( header = header, - url = streamUrl, + url = episodeInfo.vidCdnUrl.orEmpty(), ).string(), + id = id.orEmpty(), ) - } - private suspend fun fetchM3u8Url( - header: Map, - url: String, - ) = (animeService as GogoAnimeService).fetchM3u8Url(header, url) + val streamUrl = "${Constants.REFERER}encrypt-ajax.php?$ajaxResponse" - fun getEncryptionKeys() = keysAndIv + return parser.parseEncryptedUrls( + fetchM3u8PreProcessor( + header = header, + url = streamUrl, + ).string(), + ) + } - private suspend fun fetchM3u8PreProcessor( - header: Map, - url: String, - ) = (animeService as GogoAnimeService).fetchM3u8PreProcessor(header, url) + private suspend fun fetchM3u8Url( + header: Map, + url: String, + ) = (animeService as GogoAnimeService).fetchM3u8Url(header, url) - suspend fun getGogoUrlFromAniListId(id: Int) = (animeService as GogoAnimeService).getGoGoDetailedUrl(id) + fun getEncryptionKeys() = keysAndIv - companion object { - val keysAndIv: Keys = - Keys( - key = "37911490979715163134003223491201", - secondKey = "54674138327930866480207815084989", - iv = "3134003223491201", - ) - } + private suspend fun fetchM3u8PreProcessor( + header: Map, + url: String, + ) = (animeService as GogoAnimeService).fetchM3u8PreProcessor(header, url) + + suspend fun getGogoUrlFromAniListId(id: Int) = (animeService as GogoAnimeService).getGoGoDetailedUrl(id) + + companion object { + val keysAndIv: Keys = + Keys( + key = "37911490979715163134003223491201", + secondKey = "54674138327930866480207815084989", + iv = "3134003223491201", + ) } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/network/general/UpdateClient.kt b/app/src/main/java/com/kl3jvi/animity/data/network/general/UpdateClient.kt index bcd473ff..e611c261 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/network/general/UpdateClient.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/network/general/UpdateClient.kt @@ -3,9 +3,9 @@ package com.kl3jvi.animity.data.network.general import javax.inject.Inject class UpdateClient - @Inject - constructor( - private val updateService: UpdateService, - ) { - suspend fun getUpdateInfo(): VersionInfo = updateService.getUpdateInfo() - } +@Inject +constructor( + private val updateService: UpdateService, +) { + suspend fun getUpdateInfo(): VersionInfo = updateService.getUpdateInfo() +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/network/interceptor/HeaderInterceptor.kt b/app/src/main/java/com/kl3jvi/animity/data/network/interceptor/HeaderInterceptor.kt index 09b72f90..2a6120d1 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/network/interceptor/HeaderInterceptor.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/network/interceptor/HeaderInterceptor.kt @@ -7,99 +7,99 @@ import com.kl3jvi.animity.analytics.Performance import com.kl3jvi.animity.data.model.auth_models.AuthResponse import com.kl3jvi.animity.domain.repositories.LoginRepository import com.kl3jvi.animity.domain.repositories.PersistenceRepository +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Response -import javax.inject.Inject class HeaderInterceptor - @Inject - constructor( - private val loginRepository: LoginRepository, - private val localStorage: PersistenceRepository, - private val performance: Performance, // Assuming you've added this as a dependency - ) : Interceptor { - /** - * It intercepts the request and adds the bearer token to the header. - * - * @param chain Interceptor.Chain - This is the chain of interceptors that the request will go - * through. - */ - override fun intercept(chain: Interceptor.Chain): Response { - val originalRequest = chain.request() +@Inject +constructor( + private val loginRepository: LoginRepository, + private val localStorage: PersistenceRepository, + private val performance: Performance, // Assuming you've added this as a dependency +) : Interceptor { + /** + * It intercepts the request and adds the bearer token to the header. + * + * @param chain Interceptor.Chain - This is the chain of interceptors that the request will go + * through. + */ + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() - val token = - localStorage.bearerToken ?: run { - Log.d("HeaderInterceptor", "No bearer token found. Proceeding without it.") - return chain.proceed(originalRequest) - } + val token = + localStorage.bearerToken ?: run { + Log.d("HeaderInterceptor", "No bearer token found. Proceeding without it.") + return chain.proceed(originalRequest) + } - // Adding the header to the request. - val newRequest = - originalRequest.newBuilder() - .addHeader("Authorization", "Bearer $token") - .addHeader("Accept", "application/json") - .addHeader("Content-Type", "application/json") - .build() + // Adding the header to the request. + val newRequest = + originalRequest.newBuilder() + .addHeader("Authorization", "Bearer $token") + .addHeader("Accept", "application/json") + .addHeader("Content-Type", "application/json") + .build() - Log.d("HeaderInterceptor", "Added authentication headers to request.") + Log.d("HeaderInterceptor", "Added authentication headers to request.") - val response = chain.proceed(newRequest) + val response = chain.proceed(newRequest) /* Checking if the response code is 401, if it is, it will get a new access token and refresh token and then proceed with the request. */ - if (response.code == 401) { - Log.w( - "HeaderInterceptor", - "Received 401 Unauthorized response. Attempting token refresh.", - ) - localStorage.bearerToken = null + if (response.code == 401) { + Log.w( + "HeaderInterceptor", + "Received 401 Unauthorized response. Attempting token refresh.", + ) + localStorage.bearerToken = null - val refreshToken = localStorage.refreshToken + val refreshToken = localStorage.refreshToken - if (refreshToken != null) { - response.close() + if (refreshToken != null) { + response.close() - val newTokenResponse = - performance.measureAndTrace("tokenRefresh") { - runBlocking(Dispatchers.IO) { - loginRepository.refreshToken( - clientId = anilistId.toInt(), - clientSecret = anilistSecret, - refreshToken = refreshToken, - ) - }.getOrDefault(AuthResponse()) - } - - if (newTokenResponse == AuthResponse()) { - Log.e( - "HeaderInterceptor", - "Token refresh failed. Proceeding with original request.", - ) - return chain.proceed(originalRequest) + val newTokenResponse = + performance.measureAndTrace("tokenRefresh") { + runBlocking(Dispatchers.IO) { + loginRepository.refreshToken( + clientId = anilistId.toInt(), + clientSecret = anilistSecret, + refreshToken = refreshToken, + ) + }.getOrDefault(AuthResponse()) } - Log.d( - "HeaderInterceptor", - "Token refresh successful. Retrying request with new token.", - ) - localStorage.bearerToken = newTokenResponse.accessToken - localStorage.refreshToken = newTokenResponse.refreshToken - val newAuthorization = "Bearer ${newTokenResponse.accessToken}" - val retryRequest = - originalRequest.newBuilder() - .header("Authorization", newAuthorization) - .build() - - return chain.proceed(retryRequest) - } else { + if (newTokenResponse == AuthResponse()) { Log.e( "HeaderInterceptor", - "No refresh token found. Unable to refresh access token.", + "Token refresh failed. Proceeding with original request.", ) + return chain.proceed(originalRequest) } + + Log.d( + "HeaderInterceptor", + "Token refresh successful. Retrying request with new token.", + ) + localStorage.bearerToken = newTokenResponse.accessToken + localStorage.refreshToken = newTokenResponse.refreshToken + val newAuthorization = "Bearer ${newTokenResponse.accessToken}" + val retryRequest = + originalRequest.newBuilder() + .header("Authorization", newAuthorization) + .build() + + return chain.proceed(retryRequest) + } else { + Log.e( + "HeaderInterceptor", + "No refresh token found. Unable to refresh access token.", + ) } - return response } + return response } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/repository/DetailsRepositoryImpl.kt b/app/src/main/java/com/kl3jvi/animity/data/repository/DetailsRepositoryImpl.kt index ff98307c..564f310d 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/repository/DetailsRepositoryImpl.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/repository/DetailsRepositoryImpl.kt @@ -10,12 +10,14 @@ import com.kl3jvi.animity.data.network.anilist_service.AniListGraphQlClient import com.kl3jvi.animity.data.network.anime_service.base.BaseClient import com.kl3jvi.animity.data.network.general.UpdateClient import com.kl3jvi.animity.domain.repositories.DetailsRepository +import com.kl3jvi.animity.domain.repositories.PersistenceRepository import com.kl3jvi.animity.parsers.GoGoParser import com.kl3jvi.animity.persistence.EpisodeDao import com.kl3jvi.animity.settings.Settings import com.kl3jvi.animity.type.MediaListStatus import com.kl3jvi.animity.utils.logError import com.kl3jvi.animity.utils.providerFlow +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow @@ -24,104 +26,109 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import okhttp3.ResponseBody -import javax.inject.Inject @OptIn(FlowPreview::class) class DetailsRepositoryImpl - @Inject - constructor( - apiClients: Map, - private val ioDispatcher: CoroutineDispatcher, - private val episodeDao: EpisodeDao, - private val settings: Settings, - private val aniListGraphQlClient: AniListGraphQlClient, - private val updateClient: UpdateClient, - override val parser: GoGoParser, - ) : DetailsRepository { - private val selectedAnimeProvider: BaseClient? = - apiClients[settings.selectedProvider.name] +@Inject +constructor( + apiClients: Map, + private val ioDispatcher: CoroutineDispatcher, + private val episodeDao: EpisodeDao, + private val settings: Settings, + private val aniListGraphQlClient: AniListGraphQlClient, + private val updateClient: UpdateClient, + override val parser: GoGoParser, + private val persistenceRepository: PersistenceRepository +) : DetailsRepository { + private val selectedAnimeProvider: BaseClient? = + apiClients[settings.selectedProvider.name] - override fun fetchEpisodeList( - header: Map, - extra: List, - malId: Int, - episodeUrl: String, - ): Flow> { - return combine( - getListOfEpisodes(episodeUrl, extra), - getEpisodesPercentage(malId), - ) { episodeModels, episodeEntities -> + override fun fetchEpisodeList( + header: Map, + extra: List, + malId: Int, + episodeUrl: String, + ): Flow> { + return combine( + getListOfEpisodes(episodeUrl, extra), + getEpisodesPercentage(malId), + persistenceRepository.getDownloadedEpisodesForAnime(malId) + ) { episodeModels, episodeEntities, downloadedEpisodes -> - val mediaPassed = extra.first() as? AniListMedia - val reversedStreamingEpisode = mediaPassed?.streamingEpisode?.asReversed() - val episodeEntitiesMap = episodeEntities.associateBy { it.episodeUrl } + val mediaPassed = extra.first() as? AniListMedia + val reversedStreamingEpisode = mediaPassed?.streamingEpisode?.asReversed() + val episodeEntitiesMap = episodeEntities.associateBy { it.episodeUrl } - episodeModels.map { episode -> - episode.percentage = - episodeEntitiesMap[episode.episodeUrl]?.getWatchedPercentage() ?: 0 + episodeModels.map { episode -> + episode.percentage = + episodeEntitiesMap[episode.episodeUrl]?.getWatchedPercentage() ?: 0 - val matchingTitle = - reversedStreamingEpisode - ?.find { streamingEpisode -> - val cleanedTitle = - streamingEpisode.title - ?.replace("Episode", "EP") - ?.split("-") - ?.first() - ?.trim() ?: "Episode -1" - cleanedTitle == episode.episodeNumber - }?.title.orEmpty() - - episode.episodeName = matchingTitle.split("-").last().trim() - episode - } - }.flowOn(ioDispatcher) - } + val matchingTitle = + reversedStreamingEpisode + ?.find { streamingEpisode -> + val cleanedTitle = + streamingEpisode.title + ?.replace("Episode", "EP") + ?.split("-") + ?.first() + ?.trim() ?: "Episode -1" + cleanedTitle == episode.episodeNumber + }?.title.orEmpty() - override fun changeAnimeStatus( - mediaId: Int, - status: MediaListStatus, - ) = flow { - emit(aniListGraphQlClient.markAnimeStatus(mediaId, status).convert()) - } + episode.episodeName = matchingTitle.split("-").last().trim() + // change download status for episode + val downloadedEpisode = + downloadedEpisodes.find { it.episodeNumber == episode.getEpisodeNumberAsString() } + episode.isDownloaded = downloadedEpisode == null + episode + } + }.flowOn(ioDispatcher) + } - private fun getListOfEpisodes( - episodeUrl: String, - extra: List, - ) = providerFlow(settings) { provider -> - when (provider) { - AnimeTypes.GOGO_ANIME -> { - val response = selectedAnimeProvider?.fetchEpisodeList(episodeUrl) - val episodeList = - parser.fetchEpisodeList(response?.string().orEmpty()).reversed() - emit(episodeList) - } + override fun changeAnimeStatus( + mediaId: Int, + status: MediaListStatus, + ) = flow { + emit(aniListGraphQlClient.markAnimeStatus(mediaId, status).convert()) + } - AnimeTypes.ENIME -> { - val response = - selectedAnimeProvider?.fetchEpisodeList(episodeUrl, extra) - val episodeList = - response?.episodes?.map { - EpisodeModel(it.title, "Episode ${it.number}", "") - } ?: emptyList() - emit(episodeList) - } + private fun getListOfEpisodes( + episodeUrl: String, + extra: List, + ) = providerFlow(settings) { provider -> + when (provider) { + AnimeTypes.GOGO_ANIME -> { + val response = selectedAnimeProvider?.fetchEpisodeList(episodeUrl) + val episodeList = + parser.fetchEpisodeList(response?.string().orEmpty()).reversed() + emit(episodeList) } - }.catch { e -> - logError(e) - emit(emptyList()) - }.flowOn(ioDispatcher) - private fun getEpisodeTitles(id: Int) = - flow { + AnimeTypes.ENIME -> { val response = - selectedAnimeProvider?.getEpisodeTitles(id)?.episodes?.ifEmpty { emptyList() } - emit(response) - }.catch { emit(emptyList()) }.flowOn(ioDispatcher) + selectedAnimeProvider?.fetchEpisodeList(episodeUrl, extra) + val episodeList = + response?.episodes?.map { + EpisodeModel(it.title, "Episode ${it.number}", "") + } ?: emptyList() + emit(episodeList) + } + } + }.catch { e -> + logError(e) + emit(emptyList()) + }.flowOn(ioDispatcher) - private fun getEpisodesPercentage(malId: Int) = - episodeDao.getEpisodesByAnime(malId = malId) - .catch { emit(emptyList()) } + private fun getEpisodeTitles(id: Int) = + flow { + val response = + selectedAnimeProvider?.getEpisodeTitles(id)?.episodes?.ifEmpty { emptyList() } + emit(response) + }.catch { emit(emptyList()) }.flowOn(ioDispatcher) - override fun getUpdateVersionInfo() = flow { emit(updateClient.getUpdateInfo()) } - } + private fun getEpisodesPercentage(malId: Int) = + episodeDao.getEpisodesByAnime(malId = malId) + .catch { emit(emptyList()) } + + override fun getUpdateVersionInfo() = flow { emit(updateClient.getUpdateInfo()) } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/repository/FavoriteRepositoryImpl.kt b/app/src/main/java/com/kl3jvi/animity/data/repository/FavoriteRepositoryImpl.kt index 213f994e..194c5bd7 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/repository/FavoriteRepositoryImpl.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/repository/FavoriteRepositoryImpl.kt @@ -8,34 +8,34 @@ import com.kl3jvi.animity.data.network.anilist_service.AniListGraphQlClient import com.kl3jvi.animity.data.network.anime_service.gogo.GogoAnimeApiClient import com.kl3jvi.animity.data.paging.FavoritesPagingSource import com.kl3jvi.animity.domain.repositories.FavoriteRepository +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject @ExperimentalCoroutinesApi class FavoriteRepositoryImpl - @Inject - constructor( - private val apiClient: GogoAnimeApiClient, - private val aniListGraphQlClient: AniListGraphQlClient, - private val ioDispatcher: CoroutineDispatcher, - ) : FavoriteRepository { - override fun getGogoUrlFromAniListId(id: Int) = - flow { - emit(apiClient.getGogoUrlFromAniListId(id).pages?.getGogoUrl().orEmpty()) - }.flowOn(ioDispatcher) +@Inject +constructor( + private val apiClient: GogoAnimeApiClient, + private val aniListGraphQlClient: AniListGraphQlClient, + private val ioDispatcher: CoroutineDispatcher, +) : FavoriteRepository { + override fun getGogoUrlFromAniListId(id: Int) = + flow { + emit(apiClient.getGogoUrlFromAniListId(id).pages?.getGogoUrl().orEmpty()) + }.flowOn(ioDispatcher) - override fun getFavoriteAnimesFromAniList(userId: Int?): Flow> { - return Pager( - config = PagingConfig(enablePlaceholders = true, pageSize = NETWORK_PAGE_SIZE), - pagingSourceFactory = { FavoritesPagingSource(aniListGraphQlClient, userId) }, - ).flow.flowOn(ioDispatcher) - } + override fun getFavoriteAnimesFromAniList(userId: Int?): Flow> { + return Pager( + config = PagingConfig(enablePlaceholders = true, pageSize = NETWORK_PAGE_SIZE), + pagingSourceFactory = { FavoritesPagingSource(aniListGraphQlClient, userId) }, + ).flow.flowOn(ioDispatcher) + } - companion object { - const val NETWORK_PAGE_SIZE = 50 - } + companion object { + const val NETWORK_PAGE_SIZE = 50 } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/repository/FirebaseRemoteConfigRepositoryImpl.kt b/app/src/main/java/com/kl3jvi/animity/data/repository/FirebaseRemoteConfigRepositoryImpl.kt index 9b8ae59f..1131c5f0 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/repository/FirebaseRemoteConfigRepositoryImpl.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/repository/FirebaseRemoteConfigRepositoryImpl.kt @@ -9,29 +9,29 @@ import com.kl3jvi.animity.utils.logError import javax.inject.Inject class FirebaseRemoteConfigRepositoryImpl - @Inject - constructor( - override val instance: FirebaseRemoteConfig, - ) : FirebaseRemoteConfigRepository { - /** - * It fetches the remote config values from the Firebase console and sets the default values. - */ - override fun init() { - instance.setDefaultsAsync(R.xml.root_preferences) - instance.fetchAndActivate().addOnCompleteListener { task -> - if (task.isSuccessful) { - Log.e("Successfully Retrieved", "Firebase Config Url") - } else { - logError(task.exception) - } +@Inject +constructor( + override val instance: FirebaseRemoteConfig, +) : FirebaseRemoteConfigRepository { + /** + * It fetches the remote config values from the Firebase console and sets the default values. + */ + override fun init() { + instance.setDefaultsAsync(R.xml.root_preferences) + instance.fetchAndActivate().addOnCompleteListener { task -> + if (task.isSuccessful) { + Log.e("Successfully Retrieved", "Firebase Config Url") + } else { + logError(task.exception) } } + } - /** - * It returns the value of the key "BASE_URL" from the instance of the [FirebaseRemoteConfig]. - */ - override fun getBaseUrl(): String { - Log.e("Received String", instance.getString("BASE_URL")) - return String.Empty - } + /** + * It returns the value of the key "BASE_URL" from the instance of the [FirebaseRemoteConfig]. + */ + override fun getBaseUrl(): String { + Log.e("Received String", instance.getString("BASE_URL")) + return String.Empty } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/repository/HomeRepositoryImpl.kt b/app/src/main/java/com/kl3jvi/animity/data/repository/HomeRepositoryImpl.kt index af91010f..df806506 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/repository/HomeRepositoryImpl.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/repository/HomeRepositoryImpl.kt @@ -4,27 +4,27 @@ import com.kl3jvi.animity.data.mapper.convert import com.kl3jvi.animity.data.network.anilist_service.AniListGraphQlClient import com.kl3jvi.animity.data.network.anime_service.gogo.GogoAnimeApiClient import com.kl3jvi.animity.domain.repositories.HomeRepository +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject -import javax.inject.Singleton @Singleton class HomeRepositoryImpl - @Inject - constructor( - private val aniListGraphQlClient: AniListGraphQlClient, - private val animeClient: GogoAnimeApiClient, - private val ioDispatcher: CoroutineDispatcher, - ) : HomeRepository { - override fun getHomeData() = - flow { - emit(aniListGraphQlClient.getHomeData().convert()) - }.flowOn(ioDispatcher) +@Inject +constructor( + private val aniListGraphQlClient: AniListGraphQlClient, + private val animeClient: GogoAnimeApiClient, + private val ioDispatcher: CoroutineDispatcher, +) : HomeRepository { + override fun getHomeData() = + flow { + emit(aniListGraphQlClient.getHomeData().convert()) + }.flowOn(ioDispatcher) - override fun getEncryptionKeys() = - flow { - emit(animeClient.getEncryptionKeys()) - }.flowOn(ioDispatcher) - } + override fun getEncryptionKeys() = + flow { + emit(animeClient.getEncryptionKeys()) + }.flowOn(ioDispatcher) +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/repository/LoginRepositoryImpl.kt b/app/src/main/java/com/kl3jvi/animity/data/repository/LoginRepositoryImpl.kt index 6c2ad59e..92f69716 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/repository/LoginRepositoryImpl.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/repository/LoginRepositoryImpl.kt @@ -3,40 +3,40 @@ package com.kl3jvi.animity.data.repository import com.kl3jvi.animity.data.model.auth_models.AuthResponse import com.kl3jvi.animity.data.network.anilist_service.AuthClient import com.kl3jvi.animity.domain.repositories.LoginRepository +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject class LoginRepositoryImpl - @Inject - constructor( - private val authClient: AuthClient, - private val ioDispatcher: CoroutineDispatcher, - ) : LoginRepository { - override fun getAccessToken( - grantType: String, - clientId: Int, - clientSecret: String, - redirectUri: String, - code: String, - ) = flow { - emit( - authClient.getAccessToken( - grantType, - clientId, - clientSecret, - redirectUri, - code, - ), - ) - }.flowOn(ioDispatcher) +@Inject +constructor( + private val authClient: AuthClient, + private val ioDispatcher: CoroutineDispatcher, +) : LoginRepository { + override fun getAccessToken( + grantType: String, + clientId: Int, + clientSecret: String, + redirectUri: String, + code: String, + ) = flow { + emit( + authClient.getAccessToken( + grantType, + clientId, + clientSecret, + redirectUri, + code, + ), + ) + }.flowOn(ioDispatcher) - override suspend fun refreshToken( - clientId: Int, - clientSecret: String, - refreshToken: String, - ): Result { - return authClient.refreshToken(clientId, clientSecret, refreshToken) - } + override suspend fun refreshToken( + clientId: Int, + clientSecret: String, + refreshToken: String, + ): Result { + return authClient.refreshToken(clientId, clientSecret, refreshToken) } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/repository/NotificationsRepositoryImpl.kt b/app/src/main/java/com/kl3jvi/animity/data/repository/NotificationsRepositoryImpl.kt index ea119fc4..77ce6a0f 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/repository/NotificationsRepositoryImpl.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/repository/NotificationsRepositoryImpl.kt @@ -12,32 +12,32 @@ import com.kl3jvi.animity.data.mapper.convert import com.kl3jvi.animity.data.network.anilist_service.AniListGraphQlClient import com.kl3jvi.animity.data.paging.NotificationPagingSource import com.kl3jvi.animity.domain.repositories.NotificationsRepository +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject class NotificationsRepositoryImpl - @Inject - constructor( - private val aniListGraphQlClient: AniListGraphQlClient, - private val ioDispatcher: CoroutineDispatcher, - ) : NotificationsRepository { - override fun getNotifications(): Flow> { - return Pager( - config = PagingConfig(enablePlaceholders = true, pageSize = 20), - pagingSourceFactory = { NotificationPagingSource(aniListGraphQlClient) }, - ).flow.flowOn(ioDispatcher) - } - - override fun getScheduled(weekName: WeekName): Flow> = - flow { - val (startTime, endTime) = weekName.computeEpochTimesForDay() - val result = aniListGraphQlClient.getAiringAnimeForDate(startTime, endTime) - emit(result.convert()) - }.catch { e -> - Log.e("Error", e.message.toString()) - }.flowOn(ioDispatcher) +@Inject +constructor( + private val aniListGraphQlClient: AniListGraphQlClient, + private val ioDispatcher: CoroutineDispatcher, +) : NotificationsRepository { + override fun getNotifications(): Flow> { + return Pager( + config = PagingConfig(enablePlaceholders = true, pageSize = 20), + pagingSourceFactory = { NotificationPagingSource(aniListGraphQlClient) }, + ).flow.flowOn(ioDispatcher) } + + override fun getScheduled(weekName: WeekName): Flow> = + flow { + val (startTime, endTime) = weekName.computeEpochTimesForDay() + val result = aniListGraphQlClient.getAiringAnimeForDate(startTime, endTime) + emit(result.convert()) + }.catch { e -> + Log.e("Error", e.message.toString()) + }.flowOn(ioDispatcher) +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/repository/PersistenceRepositoryImpl.kt b/app/src/main/java/com/kl3jvi/animity/data/repository/PersistenceRepositoryImpl.kt index fe7061f3..c5af894a 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/repository/PersistenceRepositoryImpl.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/repository/PersistenceRepositoryImpl.kt @@ -10,136 +10,146 @@ import com.kl3jvi.animity.domain.repositories.PersistenceRepository import com.kl3jvi.animity.persistence.EpisodeDao import com.kl3jvi.animity.persistence.LocalDownloadsDao import com.kl3jvi.animity.persistence.LocalEpisodeDao -import kotlinx.coroutines.flow.Flow import javax.inject.Inject +import kotlinx.coroutines.flow.Flow class PersistenceRepositoryImpl - @Inject - constructor( - private val episodeDao: EpisodeDao, - private val sharedPreferences: SharedPreferences, - private val localEpisodeDao: LocalEpisodeDao, - private val localAnimeDao: LocalDownloadsDao, - ) : PersistenceRepository { - override suspend fun insertEpisode(episodeEntity: EpisodeEntity) = episodeDao.insertEpisode(episodeEntity) +@Inject +constructor( + private val episodeDao: EpisodeDao, + private val sharedPreferences: SharedPreferences, + private val localEpisodeDao: LocalEpisodeDao, + private val localAnimeDao: LocalDownloadsDao, +) : PersistenceRepository { + override suspend fun insertEpisode(episodeEntity: EpisodeEntity) = + episodeDao.insertEpisode(episodeEntity) + + override suspend fun updateEpisode(episodeEntity: EpisodeEntity) = + episodeDao.updateEpisode(episodeEntity) - override suspend fun updateEpisode(episodeEntity: EpisodeEntity) = episodeDao.updateEpisode(episodeEntity) + override suspend fun getEpisodeContent(episodeUrl: String): Flow = + episodeDao.getEpisodeContent(episodeUrl) - override suspend fun getEpisodeContent(episodeUrl: String): Flow = episodeDao.getEpisodeContent(episodeUrl) + override suspend fun isEpisodeOnDatabase(episodeUrl: String): Boolean = + episodeDao.isEpisodeOnDatabase(episodeUrl) - override suspend fun isEpisodeOnDatabase(episodeUrl: String): Boolean = episodeDao.isEpisodeOnDatabase(episodeUrl) + override suspend fun insertLocalEpisode(localEpisode: LocalEpisode) = + localEpisodeDao.insert(localEpisode) - override suspend fun insertLocalEpisode(localEpisode: LocalEpisode) = localEpisodeDao.insert(localEpisode) + override fun getDownloadedEpisodesForAnime(animeId: Int) = + localEpisodeDao.getEpisodesForAnime(animeId) - override suspend fun incrementDownloadedEpisodesCount(animeId: Int) = localAnimeDao.incrementDownloadedEpisodesCount(animeId) + override suspend fun incrementDownloadedEpisodesCount(animeId: Int) = + localAnimeDao.incrementDownloadedEpisodesCount(animeId) - override suspend fun insertLocalAnime(localAnime: LocalAnime) = localAnimeDao.insert(localAnime) + override suspend fun insertLocalAnime(localAnime: LocalAnime) = localAnimeDao.insert(localAnime) - override suspend fun getLocalAnimeById(aniListId: Int): LocalAnime? = localAnimeDao.getAnimeById(aniListId) + override suspend fun getLocalAnimeById(aniListId: Int): LocalAnime? = + localAnimeDao.getAnimeById(aniListId) - override fun getAllAnimesThatHasDownloadedEpisodes() = localAnimeDao.getAllAnime() + override fun getAllAnimesThatHasDownloadedEpisodes() = localAnimeDao.getAllAnime() - companion object { - private const val BEARER_TOKEN = "bearerToken" - private const val EXPIRATION = "expiration" - private const val REFRESH_TOKEN = "refreshToken" - private const val GUEST_TOKEN = "guestToken" - private const val ANILIST_USER_ID = "anilistUserId" - const val SELECTED_DNS = "dns" + companion object { + private const val BEARER_TOKEN = "bearerToken" + private const val EXPIRATION = "expiration" + private const val REFRESH_TOKEN = "refreshToken" + private const val GUEST_TOKEN = "guestToken" + private const val ANILIST_USER_ID = "anilistUserId" + const val SELECTED_DNS = "dns" - private const val ORIGIN = "https://gogoanime.gg/" - private const val REFERER = "https://goload.pro/" - private const val BASE_URL = "https://gogoanime.gg" + private const val ORIGIN = "https://gogoanime.gg/" + private const val REFERER = "https://goload.pro/" + private const val BASE_URL = "https://gogoanime.gg" + } + + override var expiration: Int? + get() = getDataInt(EXPIRATION) + set(value) { + setDataInt(EXPIRATION, value ?: -1) } - override var expiration: Int? - get() = getDataInt(EXPIRATION) - set(value) { - setDataInt(EXPIRATION, value ?: -1) - } - - override var bearerToken: String? - get() = getData(BEARER_TOKEN) - set(value) { - setData(BEARER_TOKEN, value) - } - - override var refreshToken: String? - get() = getData(REFRESH_TOKEN) - set(value) { - setData(REFRESH_TOKEN, value) - } - - override var guestToken: String? - get() = getData(GUEST_TOKEN) - set(value) { - setData(GUEST_TOKEN, value) - } - - override var aniListUserId: String? - get() = getData(ANILIST_USER_ID) - set(value) { - setData(ANILIST_USER_ID, value) - } - - override var dns: Int? - get() = getData(SELECTED_DNS)?.toInt() - set(value) { - setData(SELECTED_DNS, value.toString()) - } - - override var iv: String? - get() = getData(ORIGIN) - set(value) { - setData(ORIGIN, value) - } - - override var key: String? - get() = getData(REFERER) - set(value) { - setData(REFERER, value) - } - - override var secondKey: String? - get() = getData(BASE_URL) - set(value) { - setData(BASE_URL, value) - } - - private fun getData(key: String): String? { - return sharedPreferences.getString(key, null) + override var bearerToken: String? + get() = getData(BEARER_TOKEN) + set(value) { + setData(BEARER_TOKEN, value) } - private fun getDataInt(key: String): Int? { - return sharedPreferences.getInt(key, -1) + override var refreshToken: String? + get() = getData(REFRESH_TOKEN) + set(value) { + setData(REFRESH_TOKEN, value) } - private fun setData( - key: String, - value: String?, - ) { - sharedPreferences.edit().putString(key, value).apply() + override var guestToken: String? + get() = getData(GUEST_TOKEN) + set(value) { + setData(GUEST_TOKEN, value) } - private fun setDataInt( - key: String, - value: Int, - ) { - sharedPreferences.edit().putInt(key, value).apply() + override var aniListUserId: String? + get() = getData(ANILIST_USER_ID) + set(value) { + setData(ANILIST_USER_ID, value) } - override fun clearStorage(triggered: () -> Unit) { - sharedPreferences.edit { - clear() - triggered() - } + override var dns: Int? + get() = getData(SELECTED_DNS)?.toInt() + set(value) { + setData(SELECTED_DNS, value.toString()) } - inline fun String?.fromJson(): T { - return Gson().fromJson(this.orEmpty(), T::class.java) + override var iv: String? + get() = getData(ORIGIN) + set(value) { + setData(ORIGIN, value) } - inline fun T?.toJson(): String? { - return Gson().toJson(this) + override var key: String? + get() = getData(REFERER) + set(value) { + setData(REFERER, value) } + + override var secondKey: String? + get() = getData(BASE_URL) + set(value) { + setData(BASE_URL, value) + } + + private fun getData(key: String): String? { + return sharedPreferences.getString(key, null) + } + + private fun getDataInt(key: String): Int? { + return sharedPreferences.getInt(key, -1) + } + + private fun setData( + key: String, + value: String?, + ) { + sharedPreferences.edit().putString(key, value).apply() + } + + private fun setDataInt( + key: String, + value: Int, + ) { + sharedPreferences.edit().putInt(key, value).apply() + } + + override fun clearStorage(triggered: () -> Unit) { + sharedPreferences.edit { + clear() + triggered() + } + } + + inline fun String?.fromJson(): T { + return Gson().fromJson(this.orEmpty(), T::class.java) + } + + inline fun T?.toJson(): String? { + return Gson().toJson(this) } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/repository/PlayerRepositoryImpl.kt b/app/src/main/java/com/kl3jvi/animity/data/repository/PlayerRepositoryImpl.kt index 0f2467f4..c41b8b72 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/repository/PlayerRepositoryImpl.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/repository/PlayerRepositoryImpl.kt @@ -9,56 +9,56 @@ import com.kl3jvi.animity.domain.repositories.PlayerRepository import com.kl3jvi.animity.parsers.GoGoParser import com.kl3jvi.animity.persistence.EpisodeDao import com.kl3jvi.animity.settings.Settings +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext -import javax.inject.Inject class PlayerRepositoryImpl - @Inject - constructor( - apiClients: Map, - settings: Settings, - private val episodeDao: EpisodeDao, - override val parser: GoGoParser, - private val ioDispatcher: CoroutineDispatcher, - ) : PlayerRepository { - private val selectedAnimeProvider: BaseClient? = - apiClients[settings.selectedProvider.name] +@Inject +constructor( + apiClients: Map, + settings: Settings, + private val episodeDao: EpisodeDao, + override val parser: GoGoParser, + private val ioDispatcher: CoroutineDispatcher, +) : PlayerRepository { + private val selectedAnimeProvider: BaseClient? = + apiClients[settings.selectedProvider.name] - init { - Log.e("Selected", settings.selectedProvider.name) - } + init { + Log.e("Selected", settings.selectedProvider.name) + } - override fun getMediaUrl( - header: Map, - url: String, - extra: List, - ): Flow>> = - flow { - val result = - selectedAnimeProvider?.fetchEpisodeMediaUrl( - header, - url, - extra, - ) ?: emptyList>() - emit(result) - }.catch { it.printStackTrace() } + override fun getMediaUrl( + header: Map, + url: String, + extra: List, + ): Flow>> = + flow { + val result = + selectedAnimeProvider?.fetchEpisodeMediaUrl( + header, + url, + extra, + ) ?: emptyList>() + emit(result) + }.catch { it.printStackTrace() } - override suspend fun upsertEpisode(episodeEntity: EpisodeEntity) { - withContext(ioDispatcher) { - if (episodeDao.isEpisodeOnDatabase(episodeEntity.episodeUrl) && episodeEntity.watchedDuration > 0) { - episodeDao.updateEpisode(episodeEntity) - } else { - episodeDao.insertEpisode(episodeEntity) - } + override suspend fun upsertEpisode(episodeEntity: EpisodeEntity) { + withContext(ioDispatcher) { + if (episodeDao.isEpisodeOnDatabase(episodeEntity.episodeUrl) && episodeEntity.watchedDuration > 0) { + episodeDao.updateEpisode(episodeEntity) + } else { + episodeDao.insertEpisode(episodeEntity) } } + } - override suspend fun getPlaybackPosition(episodeUrl: String): Flow { - return episodeDao.getEpisodeContent(episodeUrl) - .flowOn(ioDispatcher) - .filter { episodeDao.isEpisodeOnDatabase(episodeUrl) } - } + override suspend fun getPlaybackPosition(episodeUrl: String): Flow { + return episodeDao.getEpisodeContent(episodeUrl) + .flowOn(ioDispatcher) + .filter { episodeDao.isEpisodeOnDatabase(episodeUrl) } } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/repository/ProfileRepositoryImpl.kt b/app/src/main/java/com/kl3jvi/animity/data/repository/ProfileRepositoryImpl.kt index 2ea12035..edfddb60 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/repository/ProfileRepositoryImpl.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/repository/ProfileRepositoryImpl.kt @@ -11,56 +11,56 @@ import com.kl3jvi.animity.data.model.ui_models.User import com.kl3jvi.animity.data.network.anilist_service.AniListGraphQlClient import com.kl3jvi.animity.data.paging.FollowersPagingSource import com.kl3jvi.animity.domain.repositories.ProfileRepository +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapNotNull -import javax.inject.Inject class ProfileRepositoryImpl - @Inject - constructor( - private val aniListGraphQlClient: AniListGraphQlClient, - private val ioDispatcher: CoroutineDispatcher, - ) : ProfileRepository { - override fun getProfileData(userId: Int?) = - flow { - emit(aniListGraphQlClient.getUserDataById(userId)) - }.mapNotNull(ApolloResponse::convert).run { - combine( - this, - getProfileAnimes(userId), - getNumberOfFollowingAndFollowers(), - ) { userData, profileRow, followStuff -> - ProfileData( - userData = userData, - profileRow = profileRow, - followersAndFollowing = followStuff, - ) - } - }.flowOn(ioDispatcher) +@Inject +constructor( + private val aniListGraphQlClient: AniListGraphQlClient, + private val ioDispatcher: CoroutineDispatcher, +) : ProfileRepository { + override fun getProfileData(userId: Int?) = + flow { + emit(aniListGraphQlClient.getUserDataById(userId)) + }.mapNotNull(ApolloResponse::convert).run { + combine( + this, + getProfileAnimes(userId), + getNumberOfFollowingAndFollowers(), + ) { userData, profileRow, followStuff -> + ProfileData( + userData = userData, + profileRow = profileRow, + followersAndFollowing = followStuff, + ) + } + }.flowOn(ioDispatcher) - private fun getProfileAnimes(userId: Int?) = - flow { - emit(aniListGraphQlClient.getAnimeListData(userId).convert()) - }.flowOn(ioDispatcher) + private fun getProfileAnimes(userId: Int?) = + flow { + emit(aniListGraphQlClient.getAnimeListData(userId).convert()) + }.flowOn(ioDispatcher) - private fun getNumberOfFollowingAndFollowers(): Flow, List>>> { - return Pager( - config = PagingConfig(enablePlaceholders = true, pageSize = NETWORK_PAGE_SIZE), - pagingSourceFactory = { FollowersPagingSource(aniListGraphQlClient) }, - ).flow.flowOn(ioDispatcher) - } + private fun getNumberOfFollowingAndFollowers(): Flow, List>>> { + return Pager( + config = PagingConfig(enablePlaceholders = true, pageSize = NETWORK_PAGE_SIZE), + pagingSourceFactory = { FollowersPagingSource(aniListGraphQlClient) }, + ).flow.flowOn(ioDispatcher) + } - override fun followUser(userId: Int) = - flow { - val followState = aniListGraphQlClient.followUser(userId).convert() - emit(followState) - }.flowOn(ioDispatcher) + override fun followUser(userId: Int) = + flow { + val followState = aniListGraphQlClient.followUser(userId).convert() + emit(followState) + }.flowOn(ioDispatcher) - companion object { - const val NETWORK_PAGE_SIZE = 50 - } + companion object { + const val NETWORK_PAGE_SIZE = 50 } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/repository/SearchRepositoryImpl.kt b/app/src/main/java/com/kl3jvi/animity/data/repository/SearchRepositoryImpl.kt index 2b2320a5..f37d6584 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/repository/SearchRepositoryImpl.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/repository/SearchRepositoryImpl.kt @@ -10,37 +10,37 @@ import com.kl3jvi.animity.data.network.anilist_service.AniListGraphQlClient import com.kl3jvi.animity.data.paging.SearchAniListPagingSource import com.kl3jvi.animity.data.paging.SearchUsersPagingSource import com.kl3jvi.animity.domain.repositories.SearchRepository +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject -import javax.inject.Singleton @Singleton class SearchRepositoryImpl - @Inject - constructor( - private val apiClient: AniListGraphQlClient, - private val ioDispatcher: CoroutineDispatcher, - ) : SearchRepository { - override fun fetchAniListSearchData( - query: String, - sortType: List, - ): Flow> { - return Pager( - config = PagingConfig(enablePlaceholders = true, pageSize = NETWORK_PAGE_SIZE), - pagingSourceFactory = { SearchAniListPagingSource(apiClient, query, sortType) }, - ).flow.flowOn(ioDispatcher) - } +@Inject +constructor( + private val apiClient: AniListGraphQlClient, + private val ioDispatcher: CoroutineDispatcher, +) : SearchRepository { + override fun fetchAniListSearchData( + query: String, + sortType: List, + ): Flow> { + return Pager( + config = PagingConfig(enablePlaceholders = true, pageSize = NETWORK_PAGE_SIZE), + pagingSourceFactory = { SearchAniListPagingSource(apiClient, query, sortType) }, + ).flow.flowOn(ioDispatcher) + } - override fun fetchAniListUsers(query: String): Flow> { - return Pager( - config = PagingConfig(enablePlaceholders = true, pageSize = NETWORK_PAGE_SIZE), - pagingSourceFactory = { SearchUsersPagingSource(apiClient, query) }, - ).flow.flowOn(ioDispatcher) - } + override fun fetchAniListUsers(query: String): Flow> { + return Pager( + config = PagingConfig(enablePlaceholders = true, pageSize = NETWORK_PAGE_SIZE), + pagingSourceFactory = { SearchUsersPagingSource(apiClient, query) }, + ).flow.flowOn(ioDispatcher) + } - companion object { - const val NETWORK_PAGE_SIZE = 50 - } + companion object { + const val NETWORK_PAGE_SIZE = 50 } +} diff --git a/app/src/main/java/com/kl3jvi/animity/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/kl3jvi/animity/data/repository/UserRepositoryImpl.kt index 37d2e490..be193ab7 100644 --- a/app/src/main/java/com/kl3jvi/animity/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/kl3jvi/animity/data/repository/UserRepositoryImpl.kt @@ -3,69 +3,69 @@ package com.kl3jvi.animity.data.repository import com.kl3jvi.animity.data.network.anilist_service.AniListGraphQlClient import com.kl3jvi.animity.domain.repositories.PersistenceRepository import com.kl3jvi.animity.domain.repositories.UserRepository +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject class UserRepositoryImpl - @Inject - constructor( - private val storage: PersistenceRepository, - private val aniListGraphQlClient: AniListGraphQlClient, - private val ioDispatcher: CoroutineDispatcher, - ) : UserRepository { - override val bearerToken: String? - get() = storage.bearerToken +@Inject +constructor( + private val storage: PersistenceRepository, + private val aniListGraphQlClient: AniListGraphQlClient, + private val ioDispatcher: CoroutineDispatcher, +) : UserRepository { + override val bearerToken: String? + get() = storage.bearerToken - override val refreshToken: String? - get() = storage.refreshToken + override val refreshToken: String? + get() = storage.refreshToken - override val guestToken: String? - get() = storage.guestToken + override val guestToken: String? + get() = storage.guestToken - override val isAuthenticated: Boolean - get() = storage.bearerToken != null + override val isAuthenticated: Boolean + get() = storage.bearerToken != null - override val isGuest: Boolean - get() = storage.guestToken != null + override val isGuest: Boolean + get() = storage.guestToken != null - override val userId: String? - get() = storage.aniListUserId + override val userId: String? + get() = storage.aniListUserId - override val expiration: Int? - get() = storage.expiration + override val expiration: Int? + get() = storage.expiration - override fun setBearerToken(authToken: String?) { - storage.bearerToken = authToken - } + override fun setBearerToken(authToken: String?) { + storage.bearerToken = authToken + } - override fun setRefreshToken(refreshToken: String?) { - storage.refreshToken = refreshToken - } + override fun setRefreshToken(refreshToken: String?) { + storage.refreshToken = refreshToken + } - override fun setAniListUserId(sync: String?) { - storage.aniListUserId = sync - } + override fun setAniListUserId(sync: String?) { + storage.aniListUserId = sync + } - override fun setExpirationTime(expiration: Int) { - storage.expiration = expiration - } + override fun setExpirationTime(expiration: Int) { + storage.expiration = expiration + } - override fun setProvider(provider: String) { - } + override fun setProvider(provider: String) { + } - override fun clearStorage(triggered: () -> Unit) { - storage.clearStorage(triggered) - } + override fun clearStorage(triggered: () -> Unit) { + storage.clearStorage(triggered) + } - override fun getSessionForUser() = - flow { - emit(aniListGraphQlClient.getSessionForUser()) - }.flowOn(ioDispatcher) + override fun getSessionForUser() = + flow { + emit(aniListGraphQlClient.getSessionForUser()) + }.flowOn(ioDispatcher) - override fun markAnimeAsFavorite(idAniList: Int?) = - flow { - emit(aniListGraphQlClient.markAnimeAsFavorite(idAniList)) - }.flowOn(ioDispatcher) - } + override fun markAnimeAsFavorite(idAniList: Int?) = + flow { + emit(aniListGraphQlClient.markAnimeAsFavorite(idAniList)) + }.flowOn(ioDispatcher) +} diff --git a/app/src/main/java/com/kl3jvi/animity/di/DispatcherModule.kt b/app/src/main/java/com/kl3jvi/animity/di/DispatcherModule.kt index e6748468..2a25142c 100644 --- a/app/src/main/java/com/kl3jvi/animity/di/DispatcherModule.kt +++ b/app/src/main/java/com/kl3jvi/animity/di/DispatcherModule.kt @@ -5,13 +5,13 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asCoroutineDispatcher import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import javax.inject.Singleton +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/com/kl3jvi/animity/di/ExoPlayerModule.kt b/app/src/main/java/com/kl3jvi/animity/di/ExoPlayerModule.kt index 645ecbf7..cfa1cdba 100644 --- a/app/src/main/java/com/kl3jvi/animity/di/ExoPlayerModule.kt +++ b/app/src/main/java/com/kl3jvi/animity/di/ExoPlayerModule.kt @@ -5,7 +5,10 @@ package com.kl3jvi.animity.di import android.content.Context import com.google.android.exoplayer2.database.ExoDatabaseProvider import com.google.android.exoplayer2.offline.DownloadManager +import com.google.android.exoplayer2.scheduler.Requirements +import com.google.android.exoplayer2.upstream.DataSource import com.google.android.exoplayer2.upstream.DefaultHttpDataSource +import com.google.android.exoplayer2.upstream.cache.CacheDataSource import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor import com.google.android.exoplayer2.upstream.cache.SimpleCache import com.kl3jvi.animity.data.downloader.Downloader @@ -39,20 +42,46 @@ object ExoPlayerModule { return ExoDatabaseProvider(context) } + @Provides + @Singleton + fun provideDataSourceFactory( + simpleCache: SimpleCache, + ): DataSource.Factory { + return DefaultHttpDataSource.Factory() + .setUserAgent("Animity/1.0.0 (Linux;Android 11) ExoPlayerLib/2.14.1") + .setConnectTimeoutMs(10_000) + .setReadTimeoutMs(10_000) + } + + @Provides + @Singleton + fun provideCacheDataSource( + simpleCache: SimpleCache, + dataSource: DataSource.Factory + ): CacheDataSource.Factory { + return CacheDataSource.Factory() + .setCache(simpleCache) + .setUpstreamDataSourceFactory(dataSource) + } + @Provides @Singleton fun provideDownloadManager( @ApplicationContext context: Context, downloadCache: SimpleCache, databaseProvider: ExoDatabaseProvider, + httpDataSource: DataSource.Factory ): DownloadManager { return DownloadManager( context, databaseProvider, downloadCache, - DefaultHttpDataSource.Factory(), + httpDataSource, Executors.newFixedThreadPool(6), - ) + ).apply { + requirements = Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW) + maxParallelDownloads = 3 + } } @Provides diff --git a/app/src/main/java/com/kl3jvi/animity/di/GlideModule.kt b/app/src/main/java/com/kl3jvi/animity/di/GlideModule.kt index c00b9c93..fabda577 100644 --- a/app/src/main/java/com/kl3jvi/animity/di/GlideModule.kt +++ b/app/src/main/java/com/kl3jvi/animity/di/GlideModule.kt @@ -11,9 +11,9 @@ import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.module.AppGlideModule import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.signature.ObjectKey -import okhttp3.OkHttpClient import java.io.InputStream import java.util.concurrent.TimeUnit +import okhttp3.OkHttpClient @GlideModule class GlideModule : AppGlideModule() { diff --git a/app/src/main/java/com/kl3jvi/animity/di/NetworkModule.kt b/app/src/main/java/com/kl3jvi/animity/di/NetworkModule.kt index 90def752..be02cd86 100644 --- a/app/src/main/java/com/kl3jvi/animity/di/NetworkModule.kt +++ b/app/src/main/java/com/kl3jvi/animity/di/NetworkModule.kt @@ -35,14 +35,14 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoMap import dagger.multibindings.StringKey -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit import javax.inject.Named import javax.inject.Provider import javax.inject.Singleton +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/com/kl3jvi/animity/domain/repositories/PersistenceRepository.kt b/app/src/main/java/com/kl3jvi/animity/domain/repositories/PersistenceRepository.kt index 230cc3b3..cecb5c00 100644 --- a/app/src/main/java/com/kl3jvi/animity/domain/repositories/PersistenceRepository.kt +++ b/app/src/main/java/com/kl3jvi/animity/domain/repositories/PersistenceRepository.kt @@ -36,4 +36,5 @@ interface PersistenceRepository { fun getAllAnimesThatHasDownloadedEpisodes(): Flow> suspend fun getLocalAnimeById(aniListId: Int): LocalAnime? + fun getDownloadedEpisodesForAnime(animeId: Int): Flow> } diff --git a/app/src/main/java/com/kl3jvi/animity/parsers/GoGoParser.kt b/app/src/main/java/com/kl3jvi/animity/parsers/GoGoParser.kt index ef8804c6..abeaa4cf 100644 --- a/app/src/main/java/com/kl3jvi/animity/parsers/GoGoParser.kt +++ b/app/src/main/java/com/kl3jvi/animity/parsers/GoGoParser.kt @@ -6,155 +6,155 @@ import com.kl3jvi.animity.data.model.ui_models.EpisodeInfo import com.kl3jvi.animity.data.model.ui_models.EpisodeModel import com.kl3jvi.animity.domain.repositories.PersistenceRepository import com.kl3jvi.animity.utils.asyncMap -import org.json.JSONObject -import org.jsoup.Jsoup import java.util.Base64 import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec import javax.inject.Inject +import org.json.JSONObject +import org.jsoup.Jsoup class GoGoParser - @Inject - constructor( - private val preferences: PersistenceRepository, - ) : BaseParser() { - override val name: String - get() = "GOGO_ANIME" - - override suspend fun fetchEpisodeList(response: String): List { - val document = Jsoup.parse(response) - val lists = document.select("li") - return lists.asyncMap { - val episodeUrl = it.select("a").first()?.attr("href")?.trim() - val episodeNumber = it.getElementsByClass("name").first()?.text() - val episodeType = it.getElementsByClass("cate").first()?.text() - EpisodeModel( - episodeNumber = episodeNumber ?: "", - episodeType = episodeType ?: "", - episodeUrl = episodeUrl ?: "", - ) - } - } +@Inject +constructor( + private val preferences: PersistenceRepository, +) : BaseParser() { + override val name: String + get() = "GOGO_ANIME" - fun parseAnimeInfo(response: String): AnimeInfoModel { - val document = Jsoup.parse(response) - val episodeInfo = document.getElementById("episode_page") - val episodeList = episodeInfo?.select("a")?.last() - val endEpisode = episodeList?.attr("ep_end").orEmpty() - val alias = document.getElementById("alias_anime")?.attr("value").orEmpty() - val id = document.getElementById("movie_id")?.attr("value").orEmpty() - return AnimeInfoModel( - id = id, - alias = alias, - endEpisode = endEpisode, + override suspend fun fetchEpisodeList(response: String): List { + val document = Jsoup.parse(response) + val lists = document.select("li") + return lists.asyncMap { + val episodeUrl = it.select("a").first()?.attr("href")?.trim() + val episodeNumber = it.getElementsByClass("name").first()?.text() + val episodeType = it.getElementsByClass("cate").first()?.text() + EpisodeModel( + episodeNumber = episodeNumber ?: "", + episodeType = episodeType ?: "", + episodeUrl = episodeUrl ?: "", ) } + } - fun parseMediaUrl(response: String): EpisodeInfo { - val mediaUrl: String? - val document = Jsoup.parse(response) - val info = document.getElementsByClass("vidcdn").first()?.select("a") - mediaUrl = info?.attr("data-video").toString() - val nextEpisodeUrl = - document.getElementsByClass("anime_video_body_episodes_r").select("a").first() - ?.attr("href") - val previousEpisodeUrl = - document.getElementsByClass("anime_video_body_episodes_l").select("a").first() - ?.attr("href") + fun parseAnimeInfo(response: String): AnimeInfoModel { + val document = Jsoup.parse(response) + val episodeInfo = document.getElementById("episode_page") + val episodeList = episodeInfo?.select("a")?.last() + val endEpisode = episodeList?.attr("ep_end").orEmpty() + val alias = document.getElementById("alias_anime")?.attr("value").orEmpty() + val id = document.getElementById("movie_id")?.attr("value").orEmpty() + return AnimeInfoModel( + id = id, + alias = alias, + endEpisode = endEpisode, + ) + } - return EpisodeInfo( - nextEpisodeUrl = nextEpisodeUrl, - previousEpisodeUrl = previousEpisodeUrl, - vidCdnUrl = mediaUrl, - ) - } + fun parseMediaUrl(response: String): EpisodeInfo { + val mediaUrl: String? + val document = Jsoup.parse(response) + val info = document.getElementsByClass("vidcdn").first()?.select("a") + mediaUrl = info?.attr("data-video").toString() + val nextEpisodeUrl = + document.getElementsByClass("anime_video_body_episodes_r").select("a").first() + ?.attr("href") + val previousEpisodeUrl = + document.getElementsByClass("anime_video_body_episodes_l").select("a").first() + ?.attr("href") - fun parseEncryptAjax( - response: String, - id: String, - ): String { - return try { - val document = Jsoup.parse(response) - val value2 = document.select("script[data-name=\"episode\"]").attr("data-value") - val decrypt = - decryptAES( - value2, - preferences.key.toString(), - preferences.iv.toString(), - ).replace("\t", "").substringAfter(id) - val encrypted = - encryptAes( - id, - preferences.key.toString(), - preferences.iv.toString(), - ) - "id=$encrypted$decrypt&alias=$id" - } catch (e: java.lang.Exception) { - e.toString() - } - } + return EpisodeInfo( + nextEpisodeUrl = nextEpisodeUrl, + previousEpisodeUrl = previousEpisodeUrl, + vidCdnUrl = mediaUrl, + ) + } - private fun encryptAes( - text: String, - key: String, - iv: String, - ): String { - val ix = IvParameterSpec(iv.toByteArray()) - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - val secretKey = SecretKeySpec(key.toByteArray(), "AES") - cipher.init(Cipher.ENCRYPT_MODE, secretKey, ix) - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - Base64.getEncoder().encodeToString(cipher.doFinal(text.toByteArray())) - } else { - android.util.Base64.encodeToString( - cipher.doFinal(text.toByteArray()), - android.util.Base64.DEFAULT, + fun parseEncryptAjax( + response: String, + id: String, + ): String { + return try { + val document = Jsoup.parse(response) + val value2 = document.select("script[data-name=\"episode\"]").attr("data-value") + val decrypt = + decryptAES( + value2, + preferences.key.toString(), + preferences.iv.toString(), + ).replace("\t", "").substringAfter(id) + val encrypted = + encryptAes( + id, + preferences.key.toString(), + preferences.iv.toString(), ) - } + "id=$encrypted$decrypt&alias=$id" + } catch (e: java.lang.Exception) { + e.toString() + } + } + + private fun encryptAes( + text: String, + key: String, + iv: String, + ): String { + val ix = IvParameterSpec(iv.toByteArray()) + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + val secretKey = SecretKeySpec(key.toByteArray(), "AES") + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ix) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Base64.getEncoder().encodeToString(cipher.doFinal(text.toByteArray())) + } else { + android.util.Base64.encodeToString( + cipher.doFinal(text.toByteArray()), + android.util.Base64.DEFAULT, + ) } + } - private fun decryptAES( - encrypted: String, - key: String, - iv: String, - ): String { - val ix = IvParameterSpec(iv.toByteArray()) - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - val secretKey = SecretKeySpec(key.toByteArray(Charsets.UTF_8), "AES") - cipher.init(Cipher.DECRYPT_MODE, secretKey, ix) - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - String(cipher.doFinal(Base64.getDecoder().decode(encrypted))) - } else { - String( - cipher.doFinal( - android.util.Base64.decode( - encrypted, - android.util.Base64.URL_SAFE, - ), + private fun decryptAES( + encrypted: String, + key: String, + iv: String, + ): String { + val ix = IvParameterSpec(iv.toByteArray()) + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + val secretKey = SecretKeySpec(key.toByteArray(Charsets.UTF_8), "AES") + cipher.init(Cipher.DECRYPT_MODE, secretKey, ix) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String(cipher.doFinal(Base64.getDecoder().decode(encrypted))) + } else { + String( + cipher.doFinal( + android.util.Base64.decode( + encrypted, + android.util.Base64.URL_SAFE, ), - ) - } + ), + ) } + } - fun parseEncryptedUrls(response: String): List { - val urls = mutableListOf() - val data = JSONObject(response).getString("data") - val decryptedData = - decryptAES( - data, - preferences.secondKey.toString(), - preferences.iv.toString(), - ).replace( - """o" { + val urls = mutableListOf() + val data = JSONObject(response).getString("data") + val decryptedData = + decryptAES( + data, + preferences.secondKey.toString(), + preferences.iv.toString(), + ).replace( + """o" - binding.wrapper.isVisible = isConnected - binding.noInternetStatus.noInternet.isVisible = !isConnected - } - } private fun setBottomBarVisibility() { navController.addOnDestinationChangedListener { _, destination, _ -> @@ -209,6 +181,7 @@ class MainActivity : AppCompatActivity(), BottomNavScrollListener { R.id.settingsFragment, R.id.scheduleFragment, R.id.theirProfile, + R.id.navigation_downloaded_episodes ) ) { hideBottomNavBar() @@ -219,6 +192,9 @@ class MainActivity : AppCompatActivity(), BottomNavScrollListener { } override fun onScrollDown() = hideBottomNavBar() - override fun onScrollUp() = showBottomNavBar() + + companion object { + const val ANIMATION_DURATION = 200L + } } diff --git a/app/src/main/java/com/kl3jvi/animity/ui/activities/main/MainViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/activities/main/MainViewModel.kt index 86bf4082..31eeab03 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/activities/main/MainViewModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/activities/main/MainViewModel.kt @@ -18,45 +18,45 @@ import javax.inject.Inject @ExperimentalCoroutinesApi @HiltViewModel class MainViewModel - @Inject - constructor( - private val homeRepository: HomeRepository, - private val userRepository: UserRepository, - private val localStorage: PersistenceRepository, - private val ioDispatcher: CoroutineDispatcher, - network: NetworkMonitor, - ) : ViewModel() { - val isConnectedToNetwork = network.isConnected +@Inject +constructor( + private val homeRepository: HomeRepository, + private val userRepository: UserRepository, + private val localStorage: PersistenceRepository, + private val ioDispatcher: CoroutineDispatcher, + private val network: NetworkMonitor, +) : ViewModel() { - init { - viewModelScope.launch(ioDispatcher) { - launch { getUserSession() } - launch { updateEncryptionKeys() } - } + init { + viewModelScope.launch(ioDispatcher) + { + launch { getUserSession() } + launch { updateEncryptionKeys() } } + } - private fun getUserSession() { - userRepository.getSessionForUser() - .onEach { data -> - if (!data.hasErrors()) { - userRepository.setAniListUserId(data.data?.viewer?.id.toString()) - } else { - Log.e("MainViewModel", "Error getting user session") - } - }.launchIn(viewModelScope) - } + private fun getUserSession() { + userRepository.getSessionForUser() + .onEach { data -> + if (!data.hasErrors()) { + userRepository.setAniListUserId(data.data?.viewer?.id.toString()) + } else { + Log.e("MainViewModel", "Error getting user session") + } + }.launchIn(viewModelScope) + } - /** - * > It gets the encryption keys from the server and saves them to the local storage - */ - private fun updateEncryptionKeys() { - homeRepository.getEncryptionKeys() - .onEach { data -> - with(localStorage) { - iv = data.iv - key = data.key - secondKey = data.secondKey - } - }.launchIn(viewModelScope) - } + /** + * > It gets the encryption keys from the server and saves them to the local storage + */ + private fun updateEncryptionKeys() { + homeRepository.getEncryptionKeys() + .onEach { data -> + with(localStorage) { + iv = data.iv + key = data.key + secondKey = data.secondKey + } + }.launchIn(viewModelScope) } +} diff --git a/app/src/main/java/com/kl3jvi/animity/ui/activities/player/PlayerActivity.kt b/app/src/main/java/com/kl3jvi/animity/ui/activities/player/PlayerActivity.kt index 21e28383..d6d909f1 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/activities/player/PlayerActivity.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/activities/player/PlayerActivity.kt @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource import com.google.android.exoplayer2.source.ConcatenatingMediaSource +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource @@ -50,6 +51,7 @@ import com.kl3jvi.animity.utils.Constants import com.kl3jvi.animity.utils.Constants.Companion.ANILIST_ID import com.kl3jvi.animity.utils.Constants.Companion.ANIME_TITLE import com.kl3jvi.animity.utils.Constants.Companion.EPISODE_DETAILS +import com.kl3jvi.animity.utils.Constants.Companion.EPISODE_URL import com.kl3jvi.animity.utils.Constants.Companion.REFERER import com.kl3jvi.animity.utils.Constants.Companion.THUMBNAIL import com.kl3jvi.animity.utils.Constants.Companion.showSnack @@ -57,21 +59,25 @@ import com.kl3jvi.animity.utils.UiResult import com.kl3jvi.animity.utils.collect import com.kl3jvi.animity.utils.logError import dagger.hilt.android.AndroidEntryPoint +import java.io.File +import java.net.InetAddress +import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import okhttp3.Cache import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.dnsoverhttps.DnsOverHttps -import java.io.File -import java.net.InetAddress -import javax.inject.Inject +@Suppress("DEPRECATION") @ExperimentalCoroutinesApi @AndroidEntryPoint class PlayerActivity : AppCompatActivity() { @Inject lateinit var settings: Settings + @Inject + lateinit var cacheDataSourceFactory: CacheDataSource.Factory + private val binding by lazy(LazyThreadSafetyMode.NONE) { ActivityPlayerBinding.inflate(layoutInflater) } @@ -95,15 +101,17 @@ class PlayerActivity : AppCompatActivity() { lateinit var episodeUrlLocal: String lateinit var episodeEntity: EpisodeEntity private var aniListId: Int = 0 + private var episodeUrSavedInCache: String = "" private var thumbnailUri: String = "" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) - if (intent.hasExtra(EPISODE_DETAILS)) { + if (intent.hasExtra(EPISODE_DETAILS) || intent.hasExtra(EPISODE_URL)) { val getIntentData = intent.getParcelableExtra(EPISODE_DETAILS) aniListId = intent.getIntExtra(ANILIST_ID, 0) + episodeUrSavedInCache = intent.getStringExtra(EPISODE_URL).orEmpty() animeTitlePassed = intent.getStringExtra(ANIME_TITLE).toString() episodeNumberLocal = getIntentData?.episodeNumber.toString() episodeUrlLocal = getIntentData?.episodeUrl.toString() @@ -118,6 +126,15 @@ class PlayerActivity : AppCompatActivity() { initialisePlayerLayout() viewModel.episodeUrl.value = getIntentData?.episodeUrl.toString() hideSystemUi() + initializeVideoPlayback() + } + } + + private fun initializeVideoPlayback() { + if (episodeUrSavedInCache.isNotEmpty()) { + playLocally(listOf(listOf(episodeUrSavedInCache))) + } else { + initializePlayer() } } @@ -178,15 +195,27 @@ class PlayerActivity : AppCompatActivity() { public override fun onStart() { super.onStart() if (Util.SDK_INT > 23 && player == null) { - initializePlayer() +// initializePlayer() onIsPlayingChanged(isPlaying = true) } } + private fun playLocally(passedUrl: List>) { + try { + setupPlayer(passedUrl) + handlePlayerListener() + handleSkipIntro() + } catch (e: Exception) { + e.printStackTrace() + showSnack(binding.root, e.localizedMessage) + } + binding.loadingOverlay.visibility = View.GONE + } + public override fun onResume() { super.onResume() if (Util.SDK_INT <= 23 && player == null) { - initializePlayer() +// initializePlayer() onIsPlayingChanged(isPlaying = true) } player?.playWhenReady = true @@ -212,6 +241,10 @@ class PlayerActivity : AppCompatActivity() { @ExperimentalCoroutinesApi private fun initializePlayer() { + if (episodeUrSavedInCache.isNotEmpty()) { + playLocally(listOf(listOf(episodeUrSavedInCache))) + return + } collect(viewModel.episodeMediaUrl) { res -> when (res) { is UiResult.Error, UiResult.Loading -> { @@ -262,17 +295,25 @@ class PlayerActivity : AppCompatActivity() { .setSeekForwardIncrementMs(settings.seekForwardTime) .build().apply { binding.videoView.player = this - val mediaItems = - listOfEpisodesWithQualities.map { episodeWithQualities -> - MediaItem.fromUri(episodeWithQualities.last()) - } + val mediaItems = listOfEpisodesWithQualities.map { episodeWithQualities -> + MediaItem.fromUri(episodeWithQualities.last()) + } val concatenatedSource = ConcatenatingMediaSource() + mediaItems.forEach { mediaItem -> val videoSource = - buildMediaSource(mediaItem, mediaItem.localConfiguration?.uri.toString()) + buildMediaSource( + mediaItem, + mediaItem.localConfiguration?.uri.toString() + ) concatenatedSource.addMediaSource(videoSource) } - setMediaSource(concatenatedSource) + + setMediaSource( + DefaultMediaSourceFactory(this@PlayerActivity) + .setDataSourceFactory(cacheDataSourceFactory) + .createMediaSource(mediaItems.first()) + ) playWhenReady = true prepare() } @@ -289,7 +330,7 @@ class PlayerActivity : AppCompatActivity() { val realDurationMillis = player?.duration ?: 0 episodeEntity = EpisodeEntity().apply { - aniListId = aniListId + aniListId = this@PlayerActivity.aniListId episodeUrl = episodeUrlLocal watchedDuration = 0 duration = realDurationMillis diff --git a/app/src/main/java/com/kl3jvi/animity/ui/activities/player/PlayerViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/activities/player/PlayerViewModel.kt index 4282a254..cdb1c260 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/activities/player/PlayerViewModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/activities/player/PlayerViewModel.kt @@ -8,6 +8,7 @@ import com.kl3jvi.animity.domain.repositories.PlayerRepository import com.kl3jvi.animity.utils.Constants.Companion.Empty import com.kl3jvi.animity.utils.mapToUiState import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -19,55 +20,54 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import kotlinx.coroutines.plus -import javax.inject.Inject @HiltViewModel @ExperimentalCoroutinesApi class PlayerViewModel - @Inject - constructor( - private val playerRepository: PlayerRepository, - private val ioDispatcher: CoroutineDispatcher, - ) : ViewModel() { - var episodeUrl = MutableStateFlow(String.Empty) +@Inject +constructor( + private val playerRepository: PlayerRepository, + private val ioDispatcher: CoroutineDispatcher, +) : ViewModel() { + var episodeUrl = MutableStateFlow(String.Empty) - private var _playBackPosition = MutableStateFlow(0) - var playBackPosition = _playBackPosition.asStateFlow() + private var _playBackPosition = MutableStateFlow(0) + var playBackPosition = _playBackPosition.asStateFlow() - /** - * It creates a flow that emits the current position of the exoPlayer every second. - * - * @param exoPlayer ExoPlayer? - The ExoPlayer instance - */ - fun progress(exoPlayer: ExoPlayer?) = - flow { - exoPlayer?.currentPosition?.let { - while (it < 200000) { - emit(exoPlayer.currentPosition) - delay(1000) - } + /** + * It creates a flow that emits the current position of the exoPlayer every second. + * + * @param exoPlayer ExoPlayer? - The ExoPlayer instance + */ + fun progress(exoPlayer: ExoPlayer?) = + flow { + exoPlayer?.currentPosition?.let { + while (it < 200000) { + emit(exoPlayer.currentPosition) + delay(1000) } - }.flowOn(Dispatchers.Main) + } + }.flowOn(Dispatchers.Main) - val episodeMediaUrl = - episodeUrl.flatMapLatest { - val mapToUiState = - playerRepository.getMediaUrl( - url = it, - extra = listOf("naruto"), - ).mapToUiState(viewModelScope + ioDispatcher) - mapToUiState - } // List of episodes which have a list of qualities + val episodeMediaUrl = + episodeUrl.flatMapLatest { + val mapToUiState = + playerRepository.getMediaUrl( + url = it, + extra = listOf("naruto"), + ).mapToUiState(viewModelScope + ioDispatcher) + mapToUiState + } // List of episodes which have a list of qualities - fun upsertEpisode(episodeEntity: EpisodeEntity) { - viewModelScope.launch(ioDispatcher) { playerRepository.upsertEpisode(episodeEntity) } - } + fun upsertEpisode(episodeEntity: EpisodeEntity) { + viewModelScope.launch(ioDispatcher) { playerRepository.upsertEpisode(episodeEntity) } + } - fun getPlaybackPosition(episodeUrl: String) { - viewModelScope.launch(ioDispatcher) { - playerRepository.getPlaybackPosition(episodeUrl).collect { content -> - _playBackPosition.value = content.watchedDuration - } + fun getPlaybackPosition(episodeUrl: String) { + viewModelScope.launch(ioDispatcher) { + playerRepository.getPlaybackPosition(episodeUrl).collect { content -> + _playBackPosition.value = content.watchedDuration } } } +} diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/StateManager.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/StateManager.kt index e3e837ee..ec79d6a4 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/StateManager.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/StateManager.kt @@ -1,7 +1,29 @@ package com.kl3jvi.animity.ui.fragments -interface StateManager { - fun showLoading(show: Boolean) +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.coroutineScope +import androidx.lifecycle.flowWithLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach - fun handleError(e: Throwable) +abstract class StateManager { + abstract fun showLoadingState(show: Boolean) + + abstract fun handleErrorState(e: Throwable) + + fun handleNetworkChanges( + networkConnection: Flow, + lifecycle: Lifecycle, + callback: (isConnected: Boolean) -> Unit, + ) { + networkConnection.flowWithLifecycle(lifecycle,Lifecycle.State.RESUMED) + .onEach { isConnected -> + callback(isConnected) + }.launchIn(lifecycle.coroutineScope) + } +} + +interface StateManagerFactory { + fun create(): StateManager } diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/base/AnimityFragment.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/base/AnimityFragment.kt new file mode 100644 index 00000000..c4c82d8a --- /dev/null +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/base/AnimityFragment.kt @@ -0,0 +1,48 @@ +package com.kl3jvi.animity.ui.fragments.base + +import androidx.annotation.LayoutRes +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import com.kl3jvi.animity.ui.activities.main.MainViewModel +import com.kl3jvi.animity.ui.fragments.StateManager +import com.kl3jvi.animity.ui.fragments.StateManagerFactory +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow + +abstract class AnimityFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId), + StateManagerFactory { + + private val stateManager by lazy { create() } + private val mainViewModel by activityViewModels() + + abstract fun showLoading(show: Boolean) + abstract fun handleError(e: Throwable) + abstract fun handleNetworkChanges(isConnected: Boolean) + + override fun create(): StateManager { + return object : StateManager() { + override fun showLoadingState(show: Boolean) = showLoading(show) + override fun handleErrorState(e: Throwable) = handleError(e) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun onStart() { + super.onStart() + stateManager.handleNetworkChanges( + emptyFlow(), + lifecycle, + ::handleNetworkChanges + ) + } + + + override fun onResume() { + super.onResume() + stateManager.handleNetworkChanges( + emptyFlow(), + lifecycle, + ::handleNetworkChanges + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/details/animeDetails/DetailsFragment.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/details/animeDetails/DetailsFragment.kt index 291cae57..1725f42d 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/details/animeDetails/DetailsFragment.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/details/animeDetails/DetailsFragment.kt @@ -37,6 +37,11 @@ import com.kl3jvi.animity.utils.launchActivity import com.kl3jvi.animity.utils.parseTime import com.kl3jvi.animity.utils.setHtmlText import dagger.hilt.android.AndroidEntryPoint +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import javax.inject.Inject +import kotlin.properties.Delegates import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay @@ -44,11 +49,6 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import javax.inject.Inject -import kotlin.properties.Delegates @Suppress("DEPRECATION") @ExperimentalCoroutinesApi diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/details/animeDetails/DetailsViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/details/animeDetails/DetailsViewModel.kt index 51fe7a6a..afa04d7d 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/details/animeDetails/DetailsViewModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/details/animeDetails/DetailsViewModel.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.plus @@ -105,22 +106,20 @@ constructor( } fun downloadEpisode(episodeModel: EpisodeModel, aniListMedia: AniListMedia) { - viewModelScope.launch( - ioDispatcher + CoroutineExceptionHandler { _, throwable -> - Log.e("Error", "Download error: ${throwable.message}", throwable) - } - ) { - playerRepository.getMediaUrl(url = episodeModel.episodeUrl, extra = listOf("naruto")) - .asResult() - .ifChanged() - .collect { result -> - when (result) { - is Result.Error -> handleError(result) - is Result.Success -> handleSuccess(result, episodeModel, aniListMedia) - else -> {} - } + playerRepository.getMediaUrl(url = episodeModel.episodeUrl, extra = listOf("naruto")) + .asResult() + .ifChanged() + .onEach { result -> + when (result) { + is Result.Error -> handleError(result) + is Result.Success -> handleSuccess(result, episodeModel, aniListMedia) + else -> {} } - } + }.launchIn( + viewModelScope + ioDispatcher + CoroutineExceptionHandler { _, throwable -> + Log.e("Error", "Download error: ${throwable.message}", throwable) + } + ) } private fun handleError(result: Result.Error) { @@ -135,18 +134,14 @@ constructor( val videoUrls = result.data if (videoUrls.isNotEmpty()) { val videoUrl = videoUrls.first().first() - checkAndInsertAnime(aniListMedia) - - val localEpisode = createLocalEpisode(episodeModel, aniListMedia) - + val localEpisode = createLocalEpisode(videoUrl, episodeModel, aniListMedia) downloader.downloadVideoUrl( videoUrl, DownloadListener(localEpisode) ) - } else { - Log.e("Error", "No video URLs found.") - } + } else return + } private suspend fun checkAndInsertAnime(aniListMedia: AniListMedia) { @@ -165,11 +160,12 @@ constructor( } private fun createLocalEpisode( + videoUrl: String, episodeModel: EpisodeModel, aniListMedia: AniListMedia ): LocalEpisode { return LocalEpisode( - episodeUrl = episodeModel.episodeUrl, + episodeUrl = videoUrl, animeId = aniListMedia.idAniList, episodeNumber = episodeModel.getEpisodeNumberAsString(), downloaded = true @@ -192,7 +188,6 @@ constructor( } } - fun changeAnimeStatus(status: MediaListStatus) { passedAniListMedia.flatMapLatest { detailsRepository.changeAnimeStatus(it.idAniList, status) diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/details/animeDetails/EpisodeContainer.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/details/animeDetails/EpisodeContainer.kt index 3b57f54d..ba93c792 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/details/animeDetails/EpisodeContainer.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/details/animeDetails/EpisodeContainer.kt @@ -69,6 +69,7 @@ class EpisodeContainer : Fragment(R.layout.fragment_episode_container) { } } showTitle(episodeModel.episodeName.isNotEmpty()) + isDownloaded(episodeModel.isDownloaded) isFiller(episodeModel.isFiller) downloadStatus(DownloadState.STATE_QUEUED) imageUrl( diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/DownloadFragment.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/DownloadFragment.kt index 0f255faa..0f0dfec8 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/DownloadFragment.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/DownloadFragment.kt @@ -3,9 +3,6 @@ package com.kl3jvi.animity.ui.fragments.downloads import android.os.Bundle import android.util.Log import android.view.View -import android.view.animation.Animation -import android.view.animation.AnimationUtils -import android.widget.ImageButton import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager @@ -16,6 +13,7 @@ import com.kl3jvi.animity.utils.BottomNavScrollListener import com.kl3jvi.animity.utils.UiResult import com.kl3jvi.animity.utils.collect import com.kl3jvi.animity.utils.epoxy.setupBottomNavScrollListener +import com.kl3jvi.animity.utils.navigateSafe import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -52,40 +50,12 @@ class DownloadFragment : Fragment(R.layout.fragment_download) { id(anime.id) anime(anime) clickListener { a -> - val deleteButton = - a?.findViewById(R.id.delete_button) - - if (deleteButton?.visibility == View.VISIBLE) { - // Create a fade out animation and then set visibility to GONE - val fadeOut = - AnimationUtils.loadAnimation( - deleteButton.context, - android.R.anim.fade_out, - ) - fadeOut.setAnimationListener( - object : - Animation.AnimationListener { - override fun onAnimationStart(animation: Animation?) {} - - override fun onAnimationRepeat(animation: Animation?) {} - - override fun onAnimationEnd(animation: Animation?) { - deleteButton.visibility = View.GONE - } - }, - ) - deleteButton.startAnimation(fadeOut) - } else { - // Set visibility to VISIBLE and then create a fade in animation - deleteButton?.visibility = View.VISIBLE - val fadeIn = - AnimationUtils.loadAnimation( - deleteButton?.context, - android.R.anim.fade_in, - ) - deleteButton?.startAnimation(fadeIn) - } - true + a.navigateSafe( + DownloadFragmentDirections.actionNavigationDownloadsToDownloadedEpisodesFragment( + anime.id, + anime.title + ), + ) } } } diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/DownloadViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/DownloadViewModel.kt index 530310a3..33aa5e06 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/DownloadViewModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/DownloadViewModel.kt @@ -8,12 +8,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class DownloadViewModel - @Inject - constructor( - persistenceRepository: PersistenceRepository, - ) : ViewModel() { - val listOfAnimes = - persistenceRepository.getAllAnimesThatHasDownloadedEpisodes() - .mapToUiState(viewModelScope) - } +class DownloadViewModel @Inject constructor( + persistenceRepository: PersistenceRepository, +) : ViewModel() { + val listOfAnimes = + persistenceRepository.getAllAnimesThatHasDownloadedEpisodes() + .mapToUiState(viewModelScope) +} diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/episodes/DownloadedEpisodesFragment.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/episodes/DownloadedEpisodesFragment.kt new file mode 100644 index 00000000..3976c5ef --- /dev/null +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/episodes/DownloadedEpisodesFragment.kt @@ -0,0 +1,63 @@ +package com.kl3jvi.animity.ui.fragments.downloads.episodes + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.navArgs +import com.kl3jvi.animity.R +import com.kl3jvi.animity.databinding.FragmentDownloadedEpisodesBinding +import com.kl3jvi.animity.episodeList +import com.kl3jvi.animity.ui.activities.player.PlayerActivity +import com.kl3jvi.animity.utils.Constants +import com.kl3jvi.animity.utils.collect +import com.kl3jvi.animity.utils.launchActivity +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@AndroidEntryPoint +class DownloadedEpisodesFragment : Fragment(R.layout.fragment_downloaded_episodes) { + + companion object { + fun newInstance() = DownloadedEpisodesFragment() + } + + private val viewModel: DownloadedEpisodesViewModel by viewModels() + + private val args: DownloadedEpisodesFragmentArgs by navArgs() + private var binding: FragmentDownloadedEpisodesBinding? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentDownloadedEpisodesBinding.bind(view) + initViews() + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun initViews() { + viewModel.setAnimeId(args.animePassId) + collect(viewModel.downloadedEpisodes) { downloadList -> + binding?.epoxyRecyclerView?.withModels { + downloadList.sortedBy { it.episodeNumber }.forEach { animeDetails -> + episodeList { + id(animeDetails.episodeUrl) + episodeInfo(animeDetails) + clickListener { view -> + // navigate to player + requireContext().launchActivity { + putExtra(Constants.EPISODE_URL, animeDetails.episodeUrl) + putExtra(Constants.ANILIST_ID, animeDetails.animeId) + putExtra(Constants.ANIME_TITLE, animeDetails.episodeNumber) + } + } + } + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } +} diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/episodes/DownloadedEpisodesViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/episodes/DownloadedEpisodesViewModel.kt new file mode 100644 index 00000000..94166ffa --- /dev/null +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/downloads/episodes/DownloadedEpisodesViewModel.kt @@ -0,0 +1,25 @@ +package com.kl3jvi.animity.ui.fragments.downloads.episodes + +import androidx.lifecycle.ViewModel +import com.kl3jvi.animity.domain.repositories.PersistenceRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest + +@HiltViewModel +class DownloadedEpisodesViewModel @Inject constructor( + private val persistenceRepository: PersistenceRepository +) : ViewModel() { + private val animeId: MutableStateFlow = MutableStateFlow(-1) + val downloadedEpisodes = animeId.flatMapLatest { + if (it != -1) + persistenceRepository.getDownloadedEpisodesForAnime(it) + else emptyFlow() + } + + fun setAnimeId(id: Int) { + animeId.value = id + } +} diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/favorites/FavoritesFragment.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/favorites/FavoritesFragment.kt index 4c656fd0..ae58183a 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/favorites/FavoritesFragment.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/favorites/FavoritesFragment.kt @@ -11,7 +11,6 @@ import androidx.recyclerview.widget.GridLayoutManager import com.kl3jvi.animity.R import com.kl3jvi.animity.analytics.Analytics import com.kl3jvi.animity.databinding.FragmentFavoritesBinding -import com.kl3jvi.animity.ui.fragments.StateManager import com.kl3jvi.animity.utils.BottomNavScrollListener import com.kl3jvi.animity.utils.Constants.Companion.showSnack import com.kl3jvi.animity.utils.collect @@ -25,7 +24,7 @@ import javax.inject.Inject @Suppress("SameParameterValue") @ExperimentalCoroutinesApi @AndroidEntryPoint -class FavoritesFragment : Fragment(R.layout.fragment_favorites), StateManager { +class FavoritesFragment : Fragment(R.layout.fragment_favorites) { private val viewModel: FavoritesViewModel by viewModels() private var binding: FragmentFavoritesBinding? = null private lateinit var pagingController: FavoritesSearchController @@ -100,7 +99,7 @@ class FavoritesFragment : Fragment(R.layout.fragment_favorites), StateManager { binding = null } - override fun showLoading(show: Boolean) = Unit + fun showLoading(show: Boolean) = Unit - override fun handleError(e: Throwable) = showSnack(binding?.root, e.message) + fun handleError(e: Throwable) = showSnack(binding?.root, e.message) } diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/favorites/FavoritesViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/favorites/FavoritesViewModel.kt index 39121bc2..f4cd86bf 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/favorites/FavoritesViewModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/favorites/FavoritesViewModel.kt @@ -9,6 +9,7 @@ import com.kl3jvi.animity.data.model.ui_models.AniListMedia import com.kl3jvi.animity.domain.repositories.FavoriteRepository import com.kl3jvi.animity.domain.repositories.PersistenceRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job @@ -20,37 +21,36 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.plus -import javax.inject.Inject @ExperimentalCoroutinesApi @HiltViewModel class FavoritesViewModel - @Inject - constructor( - private val favoriteRepository: FavoriteRepository, - private val localStorage: PersistenceRepository, - private val ioDispatcher: CoroutineDispatcher, - ) : ViewModel() { - private var _favoritesList = MutableStateFlow(FavouriteState.Loading) - val favoritesList = _favoritesList.asStateFlow() - private var job: Job? = null +@Inject +constructor( + private val favoriteRepository: FavoriteRepository, + private val localStorage: PersistenceRepository, + private val ioDispatcher: CoroutineDispatcher, +) : ViewModel() { + private var _favoritesList = MutableStateFlow(FavouriteState.Loading) + val favoritesList = _favoritesList.asStateFlow() + private var job: Job? = null - init { - refreshFavorites() - } + init { + refreshFavorites() + } - fun refreshFavorites() { - favoriteRepository.getFavoriteAnimesFromAniList(localStorage.aniListUserId?.toInt()) - .cachedIn(viewModelScope) - .onStart { _favoritesList.value = FavouriteState.Loading } - .catch { _favoritesList.value = FavouriteState.Error(it) } - .buffer(0) - .onEach { - Log.e("FAVORITES", it.toString()) - _favoritesList.value = FavouriteState.Success(it) - }.launchIn(viewModelScope + ioDispatcher) - } + fun refreshFavorites() { + favoriteRepository.getFavoriteAnimesFromAniList(localStorage.aniListUserId?.toInt()) + .cachedIn(viewModelScope) + .onStart { _favoritesList.value = FavouriteState.Loading } + .catch { _favoritesList.value = FavouriteState.Error(it) } + .buffer(0) + .onEach { + Log.e("FAVORITES", it.toString()) + _favoritesList.value = FavouriteState.Success(it) + }.launchIn(viewModelScope + ioDispatcher) } +} sealed interface FavouriteState { data class Success(val data: PagingData) : FavouriteState diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/home/HomeFragment.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/home/HomeFragment.kt index 366e9ef7..171daa2c 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/home/HomeFragment.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/home/HomeFragment.kt @@ -3,13 +3,13 @@ package com.kl3jvi.animity.ui.fragments.home import android.os.Bundle import android.view.View import androidx.core.view.isVisible -import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import com.kl3jvi.animity.R import com.kl3jvi.animity.analytics.Analytics import com.kl3jvi.animity.databinding.FragmentHomeBinding -import com.kl3jvi.animity.ui.fragments.StateManager +import com.kl3jvi.animity.ui.activities.main.MainActivity +import com.kl3jvi.animity.ui.fragments.base.AnimityFragment import com.kl3jvi.animity.ui.fragments.notifications.NotificationBottomSheetFragment import com.kl3jvi.animity.ui.fragments.schedule.ScheduleViewModel import com.kl3jvi.animity.utils.BottomNavScrollListener @@ -25,10 +25,12 @@ import javax.inject.Inject @ExperimentalCoroutinesApi @AndroidEntryPoint -class HomeFragment : Fragment(R.layout.fragment_home), StateManager { +class HomeFragment : AnimityFragment(R.layout.fragment_home) { private val viewModel: HomeViewModel by viewModels() private var binding: FragmentHomeBinding? = null private val scheduleViewModel by activityViewModels() + private val stateManager by lazy { create() } + private val listener: BottomNavScrollListener by lazy { requireActivity() as BottomNavScrollListener @@ -98,16 +100,18 @@ class HomeFragment : Fragment(R.layout.fragment_home), StateManager { } } + override fun showLoading(show: Boolean) = Unit + override fun handleError(e: Throwable) = showSnack(binding?.root, e.message) + override fun handleNetworkChanges(isConnected: Boolean) { +// val activityBinding = (activity as MainActivity).binding +// activityBinding.wrapper.isVisible = isConnected +// activityBinding.noInternetStatus.noInternet.isVisible = !isConnected + } + override fun onDestroyView() { super.onDestroyView() binding = null } - override fun showLoading(show: Boolean) = - with(binding) { - this?.mainRv?.isVisible = !show - this?.loading?.isVisible = show - } - override fun handleError(e: Throwable) = showSnack(binding?.root, e.message) } diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/home/HomeViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/home/HomeViewModel.kt index bbf0af0c..5df5df5a 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/home/HomeViewModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/home/HomeViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.kl3jvi.animity.analytics.Analytics import com.kl3jvi.animity.domain.repositories.HomeRepository import com.kl3jvi.animity.utils.mapToUiState +import com.kl3jvi.animity.utils.network.NetworkMonitor import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.plus @@ -12,13 +13,13 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel - @Inject - constructor( - homeRepository: HomeRepository, - val analytics: Analytics, - ioDispatcher: CoroutineDispatcher, - ) : ViewModel() { - val homeDataUiState = - homeRepository.getHomeData() - .mapToUiState(viewModelScope + ioDispatcher) - } +@Inject +constructor( + homeRepository: HomeRepository, + val analytics: Analytics, + ioDispatcher: CoroutineDispatcher, + network: NetworkMonitor +) : ViewModel() { + val homeDataUiState = homeRepository.getHomeData() + .mapToUiState(viewModelScope + ioDispatcher, network.isConnected) +} diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/notifications/NotificationBottomSheetFragment.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/notifications/NotificationBottomSheetFragment.kt index b405bc68..ef25a5a1 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/notifications/NotificationBottomSheetFragment.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/notifications/NotificationBottomSheetFragment.kt @@ -15,7 +15,6 @@ import com.kl3jvi.animity.analytics.Analytics import com.kl3jvi.animity.data.enums.NotificationType import com.kl3jvi.animity.data.model.ui_models.AniListMedia import com.kl3jvi.animity.databinding.NotificationsBottomSheetBinding -import com.kl3jvi.animity.ui.fragments.StateManager import com.kl3jvi.animity.ui.fragments.home.HomeFragmentDirections import com.kl3jvi.animity.utils.Constants.Companion.showSnack import com.kl3jvi.animity.utils.collectLatest @@ -25,7 +24,7 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -class NotificationBottomSheetFragment : BottomSheetDialogFragment(), StateManager { +class NotificationBottomSheetFragment : BottomSheetDialogFragment() { private var binding: NotificationsBottomSheetBinding? = null private val viewModel by activityViewModels() private lateinit var pagingController: NotificationsController @@ -118,10 +117,10 @@ class NotificationBottomSheetFragment : BottomSheetDialogFragment(), StateManage showLoading(show) } - override fun showLoading(show: Boolean) { + fun showLoading(show: Boolean) { binding?.progress?.isVisible = show binding?.notificationsRv?.isVisible = !show } - override fun handleError(e: Throwable) = showSnack(binding?.root, e.message) + fun handleError(e: Throwable) = showSnack(binding?.root, e.message) } diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/notifications/NotificationViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/notifications/NotificationViewModel.kt index bbd6ced9..7e1c44bb 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/notifications/NotificationViewModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/notifications/NotificationViewModel.kt @@ -9,12 +9,12 @@ import javax.inject.Inject @HiltViewModel class NotificationViewModel - @Inject - constructor( - notificationsRepository: NotificationsRepository, - ) : ViewModel() { - val notifications = - notificationsRepository - .getNotifications() - .cachedIn(viewModelScope) - } +@Inject +constructor( + notificationsRepository: NotificationsRepository, +) : ViewModel() { + val notifications = + notificationsRepository + .getNotifications() + .cachedIn(viewModelScope) +} diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/my/ProfileFragment.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/my/ProfileFragment.kt index bc3cff31..39227096 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/my/ProfileFragment.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/my/ProfileFragment.kt @@ -19,12 +19,12 @@ import com.kl3jvi.animity.utils.createFragmentMenu import com.kl3jvi.animity.utils.epoxy.setupBottomNavScrollListener import com.kl3jvi.animity.utils.launchActivity import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.ExperimentalCoroutinesApi import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi @ExperimentalCoroutinesApi @AndroidEntryPoint -class ProfileFragment : Fragment(R.layout.fragment_profile), StateManager { +class ProfileFragment : Fragment(R.layout.fragment_profile) { private val viewModel: ProfileViewModel by viewModels() private var binding: FragmentProfileBinding? = null private lateinit var listener: BottomNavScrollListener @@ -94,10 +94,10 @@ class ProfileFragment : Fragment(R.layout.fragment_profile), StateManager { analytics.logCurrentScreen("Profile") } - override fun showLoading(show: Boolean) { + fun showLoading(show: Boolean) { binding?.loading?.isVisible = show binding?.profileRv?.isVisible = !show } - override fun handleError(e: Throwable) = showSnack(binding?.root, e.message) + fun handleError(e: Throwable) = showSnack(binding?.root, e.message) } diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/my/ProfileViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/my/ProfileViewModel.kt index d7f914db..b55cdfa9 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/my/ProfileViewModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/my/ProfileViewModel.kt @@ -6,6 +6,7 @@ import com.kl3jvi.animity.domain.repositories.PersistenceRepository import com.kl3jvi.animity.domain.repositories.ProfileRepository import com.kl3jvi.animity.domain.repositories.UserRepository import com.kl3jvi.animity.utils.mapToUiState +import com.kl3jvi.animity.utils.network.NetworkMonitor import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -15,17 +16,18 @@ import javax.inject.Inject @ExperimentalCoroutinesApi @HiltViewModel class ProfileViewModel - @Inject - constructor( - private val userRepository: UserRepository, - profileRepository: ProfileRepository, - localStorage: PersistenceRepository, - ioDispatcher: CoroutineDispatcher, - ) : ViewModel() { - val profileData = - profileRepository - .getProfileData(localStorage.aniListUserId?.toInt()) - .mapToUiState(viewModelScope + ioDispatcher) +@Inject +constructor( + private val userRepository: UserRepository, + profileRepository: ProfileRepository, + localStorage: PersistenceRepository, + ioDispatcher: CoroutineDispatcher, + networkMonitor: NetworkMonitor +) : ViewModel() { + val profileData = + profileRepository + .getProfileData(localStorage.aniListUserId?.toInt()) + .mapToUiState(viewModelScope + ioDispatcher, networkMonitor.isConnected) - fun clearStorage(triggered: () -> Unit) = userRepository.clearStorage(triggered) - } + fun clearStorage(triggered: () -> Unit) = userRepository.clearStorage(triggered) +} diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/their/TheirProfile.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/their/TheirProfile.kt index 9fed2680..0bcfa875 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/their/TheirProfile.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/their/TheirProfile.kt @@ -18,7 +18,7 @@ import com.kl3jvi.animity.utils.collect import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class TheirProfile : Fragment(R.layout.fragment_their_profile), StateManager { +class TheirProfile : Fragment(R.layout.fragment_their_profile) { private val viewModel: TheirProfileViewModel by viewModels() private var binding: FragmentTheirProfileBinding? = null @@ -71,10 +71,10 @@ class TheirProfile : Fragment(R.layout.fragment_their_profile), StateManager { binding = null } - override fun showLoading(show: Boolean) { + fun showLoading(show: Boolean) { binding?.loading?.isVisible = show binding?.theirProfileRv?.isVisible = !show } - override fun handleError(e: Throwable) = showSnack(binding?.root, e.message) + fun handleError(e: Throwable) = showSnack(binding?.root, e.message) } diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/their/TheirProfileViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/their/TheirProfileViewModel.kt index bad9e158..187c60eb 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/their/TheirProfileViewModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/profile/their/TheirProfileViewModel.kt @@ -7,41 +7,41 @@ import com.kl3jvi.animity.data.model.ui_models.User import com.kl3jvi.animity.domain.repositories.ProfileRepository import com.kl3jvi.animity.utils.mapToUiState import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class TheirProfileViewModel - @Inject - constructor( - private val profileRepository: ProfileRepository, - private val savedStateHandle: SavedStateHandle, - ) : ViewModel() { - private val followState: MutableStateFlow> = - MutableStateFlow(Pair("Follow", "")) +@Inject +constructor( + private val profileRepository: ProfileRepository, + private val savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val followState: MutableStateFlow> = + MutableStateFlow(Pair("Follow", "")) - val theirProfileData = - savedStateHandle.getStateFlow("user", User()) - .flatMapLatest { user -> - profileRepository.getProfileData(user.id) - }.combine(followState) { profileData, followState -> - profileData.copy( - userData = profileData.userData, - profileRow = profileData.profileRow, - followState = followState, - ) - }.mapToUiState(viewModelScope) + val theirProfileData = + savedStateHandle.getStateFlow("user", User()) + .flatMapLatest { user -> + profileRepository.getProfileData(user.id) + }.combine(followState) { profileData, followState -> + profileData.copy( + userData = profileData.userData, + profileRow = profileData.profileRow, + followState = followState, + ) + }.mapToUiState(viewModelScope) - fun followUser() { - val id = savedStateHandle.get("user")?.id ?: return - viewModelScope.launch(Dispatchers.IO) { - profileRepository.followUser(id).collect { newFollowState -> - followState.value = newFollowState - } + fun followUser() { + val id = savedStateHandle.get("user")?.id ?: return + viewModelScope.launch(Dispatchers.IO) { + profileRepository.followUser(id).collect { newFollowState -> + followState.value = newFollowState } } } +} diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/schedule/DayPagerAdapter.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/schedule/DayPagerAdapter.kt index d9a27d6e..db3903a7 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/schedule/DayPagerAdapter.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/schedule/DayPagerAdapter.kt @@ -12,9 +12,9 @@ class DayPagerAdapter( fragmentManager: FragmentManager, lifecycle: Lifecycle, ) : FragmentStateAdapter( - fragmentManager, - lifecycle, - ) { + fragmentManager, + lifecycle, +) { private val weekDays = WeekName.values() private var scheduleMap: Map> = emptyMap() diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/schedule/ScheduleViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/schedule/ScheduleViewModel.kt index 022044b2..ff1fc0dd 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/schedule/ScheduleViewModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/schedule/ScheduleViewModel.kt @@ -6,6 +6,7 @@ import com.kl3jvi.animity.data.enums.WeekName import com.kl3jvi.animity.data.mapper.AiringInfo import com.kl3jvi.animity.domain.repositories.NotificationsRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -13,42 +14,36 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel -class ScheduleViewModel - @Inject - constructor( - private val notificationsRepository: NotificationsRepository, - ) : ViewModel() { - private val _airingAnimeSchedule = MutableStateFlow>>(emptyMap()) - val airingAnimeSchedule: StateFlow>> = _airingAnimeSchedule +class ScheduleViewModel @Inject constructor( + private val notificationsRepository: NotificationsRepository, +) : ViewModel() { + private val _airingAnimeSchedule = MutableStateFlow>>(emptyMap()) + val airingAnimeSchedule: StateFlow>> = _airingAnimeSchedule - init { - viewModelScope.launch(Dispatchers.IO) { - fetchAiringAnimeSchedule() - } + init { + viewModelScope.launch(Dispatchers.IO) { + fetchAiringAnimeSchedule() } + } - operator fun invoke() {} + operator fun invoke() {} - private suspend fun fetchAiringAnimeSchedule() { - val results = mutableMapOf>() - // Using coroutine's coroutineScope to ensure all children coroutines complete before proceeding - coroutineScope { - WeekName.entries.map { day -> - async(Dispatchers.IO) { - day to ( - notificationsRepository.getScheduled(day) - .firstOrNull() ?: emptyList() - ) - // Assuming you want the first emission from the Flow - } - }.forEach { deferred -> - val pair = deferred.await() - results[pair.first] = pair.second + private suspend fun fetchAiringAnimeSchedule() { + val results = mutableMapOf>() + // Using coroutine's coroutineScope to ensure all children coroutines complete before proceeding + coroutineScope { + WeekName.entries.map { day -> + async(Dispatchers.IO) { + day to (notificationsRepository.getScheduled(day).firstOrNull() ?: emptyList()) + // Assuming you want the first emission from the Flow } + }.forEach { deferred -> + val pair = deferred.await() + results[pair.first] = pair.second } - _airingAnimeSchedule.compareAndSet(_airingAnimeSchedule.value, results) } + _airingAnimeSchedule.compareAndSet(_airingAnimeSchedule.value, results) } +} diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/search/SearchViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/search/SearchViewModel.kt index 3cfe950f..8d9143dd 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/search/SearchViewModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/search/SearchViewModel.kt @@ -11,6 +11,7 @@ import com.kl3jvi.animity.data.model.ui_models.User import com.kl3jvi.animity.domain.repositories.PersistenceRepository import com.kl3jvi.animity.domain.repositories.SearchRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -23,98 +24,97 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class SearchViewModel - @Inject - constructor( - private val searchRepository: SearchRepository, - localRepository: PersistenceRepository, - ioDispatcher: CoroutineDispatcher, - ) : ViewModel() { - val myId = localRepository.aniListUserId ?: "-1" - - private val _currentSearchMode = MutableStateFlow(SearchMode.ANIME) - val currentSearchMode: StateFlow = _currentSearchMode.asStateFlow() - - private val _searchList = MutableStateFlow>(PagingData.empty()) - val searchList: StateFlow> = _searchList.asStateFlow() - - private val _usersList = MutableStateFlow>(PagingData.empty()) - val usersList: StateFlow> = _usersList.asStateFlow() - - private val sortTypes = mutableListOf() - private var lastSearchQuery = "" - - // Channels and Flows for Anime and User search - private val animeSearchQueryChannel = Channel(Channel.UNLIMITED) - private val animeSearchFlow = - animeSearchQueryChannel - .receiveAsFlow() - .debounce(500) - .filter { it.query.length >= 2 } - .flatMapLatest { searchRepository.fetchAniListSearchData(it.query, it.sortTypes) } - .cachedIn(viewModelScope) - .flowOn(ioDispatcher) - - private val userSearchQueryChannel = Channel(Channel.UNLIMITED) - private val userSearchFlow = - userSearchQueryChannel - .receiveAsFlow() - .debounce(500) - .filter { it.length >= 2 } - .flatMapLatest { searchRepository.fetchAniListUsers(it) } - .cachedIn(viewModelScope) - .flowOn(ioDispatcher) - - init { - viewModelScope.launch { - animeSearchFlow.collectLatest { - _searchList.value = it - } - } - - viewModelScope.launch { - userSearchFlow.collectLatest { - _usersList.value = it - } +@Inject +constructor( + private val searchRepository: SearchRepository, + localRepository: PersistenceRepository, + ioDispatcher: CoroutineDispatcher, +) : ViewModel() { + val myId = localRepository.aniListUserId ?: "-1" + + private val _currentSearchMode = MutableStateFlow(SearchMode.ANIME) + val currentSearchMode: StateFlow = _currentSearchMode.asStateFlow() + + private val _searchList = MutableStateFlow>(PagingData.empty()) + val searchList: StateFlow> = _searchList.asStateFlow() + + private val _usersList = MutableStateFlow>(PagingData.empty()) + val usersList: StateFlow> = _usersList.asStateFlow() + + private val sortTypes = mutableListOf() + private var lastSearchQuery = "" + + // Channels and Flows for Anime and User search + private val animeSearchQueryChannel = Channel(Channel.UNLIMITED) + private val animeSearchFlow = + animeSearchQueryChannel + .receiveAsFlow() + .debounce(500) + .filter { it.query.length >= 2 } + .flatMapLatest { searchRepository.fetchAniListSearchData(it.query, it.sortTypes) } + .cachedIn(viewModelScope) + .flowOn(ioDispatcher) + + private val userSearchQueryChannel = Channel(Channel.UNLIMITED) + private val userSearchFlow = + userSearchQueryChannel + .receiveAsFlow() + .debounce(500) + .filter { it.length >= 2 } + .flatMapLatest { searchRepository.fetchAniListUsers(it) } + .cachedIn(viewModelScope) + .flowOn(ioDispatcher) + + init { + viewModelScope.launch { + animeSearchFlow.collectLatest { + _searchList.value = it } } - fun onSearchQueryChanged(query: String) { - lastSearchQuery = query.trim() - when (_currentSearchMode.value) { - SearchMode.ANIME -> searchForAnime(lastSearchQuery) - SearchMode.USERS -> searchForUsers(lastSearchQuery) + viewModelScope.launch { + userSearchFlow.collectLatest { + _usersList.value = it } } + } - private fun searchForAnime(query: String) { - animeSearchQueryChannel.trySend(SearchQuery(query, sortTypes)).isSuccess + fun onSearchQueryChanged(query: String) { + lastSearchQuery = query.trim() + when (_currentSearchMode.value) { + SearchMode.ANIME -> searchForAnime(lastSearchQuery) + SearchMode.USERS -> searchForUsers(lastSearchQuery) } + } - private fun searchForUsers(query: String) { - userSearchQueryChannel.trySend(query).isSuccess - } + private fun searchForAnime(query: String) { + animeSearchQueryChannel.trySend(SearchQuery(query, sortTypes)).isSuccess + } - fun toggleSortType(sortType: SortType) { - if (sortTypes.contains(sortType)) { - sortTypes.remove(sortType) - } else { - sortTypes.add(sortType) - } - onSearchQueryChanged(lastSearchQuery) - } + private fun searchForUsers(query: String) { + userSearchQueryChannel.trySend(query).isSuccess + } - fun switchToAnimeSearch() { - _currentSearchMode.value = SearchMode.ANIME + fun toggleSortType(sortType: SortType) { + if (sortTypes.contains(sortType)) { + sortTypes.remove(sortType) + } else { + sortTypes.add(sortType) } + onSearchQueryChanged(lastSearchQuery) + } - fun switchToUserSearch() { - _currentSearchMode.value = SearchMode.USERS - } + fun switchToAnimeSearch() { + _currentSearchMode.value = SearchMode.ANIME + } + + fun switchToUserSearch() { + _currentSearchMode.value = SearchMode.USERS } +} data class SearchQuery( val query: String, diff --git a/app/src/main/java/com/kl3jvi/animity/ui/fragments/settings/SettingsViewModel.kt b/app/src/main/java/com/kl3jvi/animity/ui/fragments/settings/SettingsViewModel.kt index 15bbb402..be9bdcf2 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/fragments/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/fragments/settings/SettingsViewModel.kt @@ -5,21 +5,21 @@ import androidx.lifecycle.viewModelScope import com.kl3jvi.animity.data.network.general.VersionInfo import com.kl3jvi.animity.domain.repositories.DetailsRepository import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject @HiltViewModel class SettingsViewModel - @Inject - constructor( - detailsRepository: DetailsRepository, - ) : ViewModel() { - val versionInfo = - detailsRepository.getUpdateVersionInfo() - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - VersionInfo(null), - ) - } +@Inject +constructor( + detailsRepository: DetailsRepository, +) : ViewModel() { + val versionInfo = + detailsRepository.getUpdateVersionInfo() + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + VersionInfo(null), + ) +} diff --git a/app/src/main/java/com/kl3jvi/animity/ui/widgets/DoubleTapPlayerView.kt b/app/src/main/java/com/kl3jvi/animity/ui/widgets/DoubleTapPlayerView.kt index 9826c1ab..1ddabecf 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/widgets/DoubleTapPlayerView.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/widgets/DoubleTapPlayerView.kt @@ -13,64 +13,134 @@ import com.google.android.exoplayer2.ui.StyledPlayerView import com.kl3jvi.animity.R class DoubleTapPlayerView - @JvmOverloads - constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, - ) : StyledPlayerView(context, attrs, defStyleAttr) { - private val gestureDetector: GestureDetectorCompat - private val gestureListener: DoubleTapGestureListener = DoubleTapGestureListener(rootView) +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : StyledPlayerView(context, attrs, defStyleAttr) { + private val gestureDetector: GestureDetectorCompat + private val gestureListener: DoubleTapGestureListener = DoubleTapGestureListener(rootView) - private var controller: PlayerDoubleTapListener? = null - get() = gestureListener.controls - set(value) { - gestureListener.controls = value - field = value - } + private var controller: PlayerDoubleTapListener? = null + get() = gestureListener.controls + set(value) { + gestureListener.controls = value + field = value + } - private var controllerRef: Int = -1 + private var controllerRef: Int = -1 - init { - gestureDetector = GestureDetectorCompat(context, gestureListener) + init { + gestureDetector = GestureDetectorCompat(context, gestureListener) - // Check whether controller is set through XML - attrs?.let { - val a = context.obtainStyledAttributes(attrs, R.styleable.DoubleTapPlayerView, 0, 0) - controllerRef = a.getResourceId(R.styleable.DoubleTapPlayerView_dtpv_controller, -1) + // Check whether controller is set through XML + attrs?.let { + val a = context.obtainStyledAttributes(attrs, R.styleable.DoubleTapPlayerView, 0, 0) + controllerRef = a.getResourceId(R.styleable.DoubleTapPlayerView_dtpv_controller, -1) - a.recycle() - } + a.recycle() } + } - /** - * If this field is set to `true` this view will handle double tapping, otherwise it will - * handle touches the same way as the original [PlayerView][com.google.android.exoplayer2.ui.PlayerView] does - */ - var isDoubleTapEnabled = true + /** + * If this field is set to `true` this view will handle double tapping, otherwise it will + * handle touches the same way as the original [PlayerView][com.google.android.exoplayer2.ui.PlayerView] does + */ + var isDoubleTapEnabled = true - /** - * Time window a double tap is active, so a followed tap is calling a gesture detector - * method instead of normal tap (see [PlayerView.onTouchEvent]) - */ - var doubleTapDelay: Long = 700 - get() = gestureListener.doubleTapDelay - set(value) { - gestureListener.doubleTapDelay = value - field = value + /** + * Time window a double tap is active, so a followed tap is calling a gesture detector + * method instead of normal tap (see [PlayerView.onTouchEvent]) + */ + var doubleTapDelay: Long = 700 + get() = gestureListener.doubleTapDelay + set(value) { + gestureListener.doubleTapDelay = value + field = value + } + + /** + * Sets the [PlayerDoubleTapListener] which handles the gesture callbacks. + * + * Primarily used for [YouTubeOverlay][com.github.vkay94.dtpv.youtube.YouTubeOverlay] + */ + fun controller(controller: PlayerDoubleTapListener) = apply { this.controller = controller } + + /** + * Returns the current state of double tapping. + */ + fun isInDoubleTapMode(): Boolean = gestureListener.isDoubleTapping + + /** + * Resets the timeout to keep in double tap mode. + * + * Called once in [PlayerDoubleTapListener.onDoubleTapStarted]. Needs to be called + * from outside if the double tap is customized / overridden to detect ongoing taps + */ + fun keepInDoubleTapMode() { + gestureListener.keepInDoubleTapMode() + } + + /** + * Cancels double tap mode instantly by calling [PlayerDoubleTapListener.onDoubleTapFinished] + */ + fun cancelInDoubleTapMode() { + gestureListener.cancelInDoubleTapMode() + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(ev: MotionEvent): Boolean { + if (isDoubleTapEnabled) { + gestureDetector.onTouchEvent(ev) + + // Do not trigger original behavior when double tapping + // otherwise the controller would show/hide - it would flack + return true + } + return super.onTouchEvent(ev) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + // If the PlayerView is set by XML then call the corresponding setter method + if (controllerRef != -1) { + try { + val view = (this.parent as View).findViewById(controllerRef) as View + if (view is PlayerDoubleTapListener) { + controller(view) + } + } catch (e: Exception) { + e.printStackTrace() + Log.e( + "DoubleTapPlayerView", + "controllerRef is either invalid or not PlayerDoubleTapListener: ${e.message}", + ) } + } + } - /** - * Sets the [PlayerDoubleTapListener] which handles the gesture callbacks. - * - * Primarily used for [YouTubeOverlay][com.github.vkay94.dtpv.youtube.YouTubeOverlay] - */ - fun controller(controller: PlayerDoubleTapListener) = apply { this.controller = controller } + /** + * Gesture Listener for double tapping + * + * For more information which methods are called in certain situations look for + * [GestureDetector.onTouchEvent][android.view.GestureDetector.onTouchEvent], + * especially for ACTION_DOWN and ACTION_UP + */ + private class DoubleTapGestureListener(private val rootView: View) : + GestureDetector.SimpleOnGestureListener() { + private val mHandler = android.os.Handler(Looper.getMainLooper()) + private val mRunnable = + Runnable { + if (DEBUG) Log.d(TAG, "Runnable called") + isDoubleTapping = false + controls?.onDoubleTapFinished() + } - /** - * Returns the current state of double tapping. - */ - fun isInDoubleTapMode(): Boolean = gestureListener.isDoubleTapping + var controls: PlayerDoubleTapListener? = null + var isDoubleTapping = false + var doubleTapDelay: Long = 650 /** * Resets the timeout to keep in double tap mode. @@ -79,149 +149,80 @@ class DoubleTapPlayerView * from outside if the double tap is customized / overridden to detect ongoing taps */ fun keepInDoubleTapMode() { - gestureListener.keepInDoubleTapMode() + isDoubleTapping = true + mHandler.removeCallbacks(mRunnable) + mHandler.postDelayed(mRunnable, doubleTapDelay) } /** * Cancels double tap mode instantly by calling [PlayerDoubleTapListener.onDoubleTapFinished] */ fun cancelInDoubleTapMode() { - gestureListener.cancelInDoubleTapMode() + mHandler.removeCallbacks(mRunnable) + isDoubleTapping = false + controls?.onDoubleTapFinished() } - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(ev: MotionEvent): Boolean { - if (isDoubleTapEnabled) { - gestureDetector.onTouchEvent(ev) - - // Do not trigger original behavior when double tapping - // otherwise the controller would show/hide - it would flack + override fun onDown(e: MotionEvent): Boolean { + // Used to override the other methods + if (isDoubleTapping) { + controls?.onDoubleTapProgressDown(e.x, e.y) return true } - return super.onTouchEvent(ev) + return super.onDown(e) } - override fun onAttachedToWindow() { - super.onAttachedToWindow() - - // If the PlayerView is set by XML then call the corresponding setter method - if (controllerRef != -1) { - try { - val view = (this.parent as View).findViewById(controllerRef) as View - if (view is PlayerDoubleTapListener) { - controller(view) - } - } catch (e: Exception) { - e.printStackTrace() - Log.e( - "DoubleTapPlayerView", - "controllerRef is either invalid or not PlayerDoubleTapListener: ${e.message}", - ) - } + override fun onSingleTapUp(e: MotionEvent): Boolean { + if (isDoubleTapping) { + if (DEBUG) Log.d(TAG, "onSingleTapUp: isDoubleTapping = true") + controls?.onDoubleTapProgressUp(e.x, e.y) + return true } + return super.onSingleTapUp(e) } - /** - * Gesture Listener for double tapping - * - * For more information which methods are called in certain situations look for - * [GestureDetector.onTouchEvent][android.view.GestureDetector.onTouchEvent], - * especially for ACTION_DOWN and ACTION_UP - */ - private class DoubleTapGestureListener(private val rootView: View) : GestureDetector.SimpleOnGestureListener() { - private val mHandler = android.os.Handler(Looper.getMainLooper()) - private val mRunnable = - Runnable { - if (DEBUG) Log.d(TAG, "Runnable called") - isDoubleTapping = false - controls?.onDoubleTapFinished() - } - - var controls: PlayerDoubleTapListener? = null - var isDoubleTapping = false - var doubleTapDelay: Long = 650 + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + // Ignore this event if double tapping is still active + // Return true needed because this method is also called if you tap e.g. three times + // in a row, therefore the controller would appear since the original behavior is + // to hide and show on single tap + if (isDoubleTapping) return true + if (DEBUG) Log.d(TAG, "onSingleTapConfirmed: isDoubleTap = false") + return rootView.performClick() + } - /** - * Resets the timeout to keep in double tap mode. - * - * Called once in [PlayerDoubleTapListener.onDoubleTapStarted]. Needs to be called - * from outside if the double tap is customized / overridden to detect ongoing taps - */ - fun keepInDoubleTapMode() { + override fun onDoubleTap(e: MotionEvent): Boolean { + // First tap (ACTION_DOWN) of both taps + if (DEBUG) Log.d(TAG, "onDoubleTap") + if (!isDoubleTapping) { isDoubleTapping = true - mHandler.removeCallbacks(mRunnable) - mHandler.postDelayed(mRunnable, doubleTapDelay) - } - - /** - * Cancels double tap mode instantly by calling [PlayerDoubleTapListener.onDoubleTapFinished] - */ - fun cancelInDoubleTapMode() { - mHandler.removeCallbacks(mRunnable) - isDoubleTapping = false - controls?.onDoubleTapFinished() - } - - override fun onDown(e: MotionEvent): Boolean { - // Used to override the other methods - if (isDoubleTapping) { - controls?.onDoubleTapProgressDown(e.x, e.y) - return true - } - return super.onDown(e) - } - - override fun onSingleTapUp(e: MotionEvent): Boolean { - if (isDoubleTapping) { - if (DEBUG) Log.d(TAG, "onSingleTapUp: isDoubleTapping = true") - controls?.onDoubleTapProgressUp(e.x, e.y) - return true - } - return super.onSingleTapUp(e) - } - - override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - // Ignore this event if double tapping is still active - // Return true needed because this method is also called if you tap e.g. three times - // in a row, therefore the controller would appear since the original behavior is - // to hide and show on single tap - if (isDoubleTapping) return true - if (DEBUG) Log.d(TAG, "onSingleTapConfirmed: isDoubleTap = false") - return rootView.performClick() + keepInDoubleTapMode() + controls?.onDoubleTapStarted(e.x, e.y) } + return true + } - override fun onDoubleTap(e: MotionEvent): Boolean { - // First tap (ACTION_DOWN) of both taps - if (DEBUG) Log.d(TAG, "onDoubleTap") - if (!isDoubleTapping) { - isDoubleTapping = true - keepInDoubleTapMode() - controls?.onDoubleTapStarted(e.x, e.y) + override fun onDoubleTapEvent(e: MotionEvent): Boolean { + // Second tap (ACTION_UP) of both taps + if (e.actionMasked == MotionEvent.ACTION_UP && isDoubleTapping) { + if (DEBUG) { + Log.d( + TAG, + "onDoubleTapEvent, ACTION_UP", + ) } + controls?.onDoubleTapProgressUp(e.x, e.y) return true } + return super.onDoubleTapEvent(e) + } - override fun onDoubleTapEvent(e: MotionEvent): Boolean { - // Second tap (ACTION_UP) of both taps - if (e.actionMasked == MotionEvent.ACTION_UP && isDoubleTapping) { - if (DEBUG) { - Log.d( - TAG, - "onDoubleTapEvent, ACTION_UP", - ) - } - controls?.onDoubleTapProgressUp(e.x, e.y) - return true - } - return super.onDoubleTapEvent(e) - } - - companion object { - private const val TAG = ".DTGListener" - private var DEBUG = true - } + companion object { + private const val TAG = ".DTGListener" + private var DEBUG = true } } +} interface PlayerDoubleTapListener { /** @@ -233,7 +234,8 @@ interface PlayerDoubleTapListener { fun onDoubleTapStarted( posX: Float, posY: Float, - ) {} + ) { + } /** * Called for each ongoing tap (also single tap) (MotionEvent#ACTION_DOWN) @@ -246,7 +248,8 @@ interface PlayerDoubleTapListener { fun onDoubleTapProgressDown( posX: Float, posY: Float, - ) {} + ) { + } /** * Called for each ongoing tap (also single tap) (MotionEvent#ACTION_UP} @@ -259,7 +262,8 @@ interface PlayerDoubleTapListener { fun onDoubleTapProgressUp( posX: Float, posY: Float, - ) {} + ) { + } /** * Called when [DoubleTapPlayerView.getDoubleTapDelay] is over diff --git a/app/src/main/java/com/kl3jvi/animity/ui/widgets/FeedItem.kt b/app/src/main/java/com/kl3jvi/animity/ui/widgets/FeedItem.kt index 483d0518..7a741dc2 100644 --- a/app/src/main/java/com/kl3jvi/animity/ui/widgets/FeedItem.kt +++ b/app/src/main/java/com/kl3jvi/animity/ui/widgets/FeedItem.kt @@ -13,34 +13,34 @@ private const val ASPECT_RATIO_HEIGHT = 18f private const val ASPECT_RATIO_WIDTH = 13f class FeedItem - @JvmOverloads - constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = 0, - ) : ShapeableImageView(context, attrs, defStyleAttr) { - private var orientation: Int = LinearLayout.HORIZONTAL +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, +) : ShapeableImageView(context, attrs, defStyleAttr) { + private var orientation: Int = LinearLayout.HORIZONTAL - init { - context.withStyledAttributes(attrs, R.styleable.FeedItem, defStyleAttr) { - orientation = getInt(R.styleable.FeedItem_android_orientation, orientation) - } + init { + context.withStyledAttributes(attrs, R.styleable.FeedItem, defStyleAttr) { + orientation = getInt(R.styleable.FeedItem_android_orientation, orientation) } + } - override fun onMeasure( - widthMeasureSpec: Int, - heightMeasureSpec: Int, - ) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - val desiredWidth: Int - val desiredHeight: Int - if (orientation == LinearLayout.VERTICAL) { - desiredHeight = measuredHeight - desiredWidth = (desiredHeight * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt() - } else { - desiredWidth = measuredWidth - desiredHeight = (desiredWidth * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt() - } - setMeasuredDimension(desiredWidth, desiredHeight) + override fun onMeasure( + widthMeasureSpec: Int, + heightMeasureSpec: Int, + ) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val desiredWidth: Int + val desiredHeight: Int + if (orientation == LinearLayout.VERTICAL) { + desiredHeight = measuredHeight + desiredWidth = (desiredHeight * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt() + } else { + desiredWidth = measuredWidth + desiredHeight = (desiredWidth * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt() } + setMeasuredDimension(desiredWidth, desiredHeight) } +} diff --git a/app/src/main/java/com/kl3jvi/animity/utils/BottomNavScrollListener.kt b/app/src/main/java/com/kl3jvi/animity/utils/BottomNavScrollListener.kt index 0799aa1c..6144b518 100644 --- a/app/src/main/java/com/kl3jvi/animity/utils/BottomNavScrollListener.kt +++ b/app/src/main/java/com/kl3jvi/animity/utils/BottomNavScrollListener.kt @@ -2,6 +2,5 @@ package com.kl3jvi.animity.utils interface BottomNavScrollListener { fun onScrollDown() - fun onScrollUp() } diff --git a/app/src/main/java/com/kl3jvi/animity/utils/Constants.kt b/app/src/main/java/com/kl3jvi/animity/utils/Constants.kt index d863d1e3..606ef64b 100644 --- a/app/src/main/java/com/kl3jvi/animity/utils/Constants.kt +++ b/app/src/main/java/com/kl3jvi/animity/utils/Constants.kt @@ -8,6 +8,7 @@ import com.google.android.material.snackbar.Snackbar class Constants { companion object { + val EPISODE_URL: String = "EPISODE_URL" const val ANIME_TITLE: String = "ANIME_TITLE" const val EPISODE_DETAILS: String = "episodeInfo" const val ANILIST_ID = "anilist_id" diff --git a/app/src/main/java/com/kl3jvi/animity/utils/UiResult.kt b/app/src/main/java/com/kl3jvi/animity/utils/UiResult.kt index 7427b944..b78f37d2 100644 --- a/app/src/main/java/com/kl3jvi/animity/utils/UiResult.kt +++ b/app/src/main/java/com/kl3jvi/animity/utils/UiResult.kt @@ -5,6 +5,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn @@ -18,13 +20,18 @@ sealed interface UiResult { data object Loading : UiResult } -fun Flow.mapToUiState(scope: CoroutineScope): StateFlow> { - return map> { - UiResult.Success(it) - }.onStart { - emit(UiResult.Loading) - }.catch { - emit(UiResult.Error(it)) +fun Flow.mapToUiState( + scope: CoroutineScope, + networkFlow: Flow = emptyFlow() +): StateFlow> { + return networkFlow.flatMapLatest { + map> { + UiResult.Success(it) + }.onStart { + emit(UiResult.Loading) + }.catch { + emit(UiResult.Error(it)) + } }.stateIn( scope = scope, started = SharingStarted.Lazily, diff --git a/app/src/main/java/com/kl3jvi/animity/utils/network/ConnectedCompat.kt b/app/src/main/java/com/kl3jvi/animity/utils/network/ConnectedCompat.kt index 638294ef..3695f0a7 100644 --- a/app/src/main/java/com/kl3jvi/animity/utils/network/ConnectedCompat.kt +++ b/app/src/main/java/com/kl3jvi/animity/utils/network/ConnectedCompat.kt @@ -9,15 +9,15 @@ object ConnectedCompat { private val IMPL: ConnectedCompatImpl init { - IMPL = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - MarshMallowImpl - } else { - BaseImpl - } + IMPL = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + MarshMallowImpl + } else { + BaseImpl + } } - fun isConnected(connectivityManager: ConnectivityManager) = IMPL.isConnected(connectivityManager) + fun isConnected(connectivityManager: ConnectivityManager) = + IMPL.isConnected(connectivityManager) internal interface ConnectedCompatImpl { fun isConnected(connectivityManager: ConnectivityManager): Boolean @@ -25,14 +25,24 @@ object ConnectedCompat { object BaseImpl : ConnectedCompatImpl { @Suppress("DEPRECATION") - override fun isConnected(connectivityManager: ConnectivityManager): Boolean = - connectivityManager.activeNetworkInfo?.isConnected ?: false + override fun isConnected(connectivityManager: ConnectivityManager): Boolean { + return try { + connectivityManager.activeNetworkInfo?.isConnected ?: false + } catch (e: Exception) { + false + } + } } object MarshMallowImpl : ConnectedCompatImpl { @RequiresApi(Build.VERSION_CODES.M) - override fun isConnected(connectivityManager: ConnectivityManager): Boolean = - connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) - ?.hasCapability(NET_CAPABILITY_INTERNET) == true + override fun isConnected(connectivityManager: ConnectivityManager): Boolean { + return try { + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + ?.hasCapability(NET_CAPABILITY_INTERNET) == true + } catch (e: Exception) { + false + } + } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/kl3jvi/animity/utils/network/NetworkMonitor.kt b/app/src/main/java/com/kl3jvi/animity/utils/network/NetworkMonitor.kt index 344d71da..09be1c63 100644 --- a/app/src/main/java/com/kl3jvi/animity/utils/network/NetworkMonitor.kt +++ b/app/src/main/java/com/kl3jvi/animity/utils/network/NetworkMonitor.kt @@ -5,48 +5,49 @@ import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import android.os.Build +import android.util.Log +import com.kl3jvi.animity.utils.network.ConnectedCompat.isConnected import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged import javax.inject.Inject +import javax.inject.Singleton -class NetworkMonitor - @Inject - constructor( - private val connectivityManager: ConnectivityManager, - ) { - // A flow that emits true when the network is available and false when it is not. - val isConnected: Flow = - callbackFlow { - val callback = - object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - super.onAvailable(network) - trySend(true) - } +@Singleton +class NetworkMonitor @Inject constructor( + private val connectivityManager: ConnectivityManager, +) { + val isConnected: Flow = + callbackFlow { + val callback = + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + trySend(true).isSuccess + } - override fun onLost(network: Network) { - trySend(false) - super.onLost(network) - } + override fun onLost(network: Network) { + trySend(false).isSuccess } - val request = - NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - } + } + val request = + NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) } - .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) - .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) - .build() - trySend(ConnectedCompat.isConnected(connectivityManager)) - connectivityManager.registerNetworkCallback(request, callback) + } + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build() + Log.d("DEBUG", "Before isConnected check") + val connectionState = isConnected(connectivityManager) + Log.d("DEBUG", "After isConnected check: $connectionState") + connectivityManager.registerNetworkCallback(request, callback) - awaitClose { - connectivityManager.unregisterNetworkCallback(callback) - } - }.distinctUntilChanged() - } + awaitClose { + connectivityManager.unregisterNetworkCallback(callback) + } + }.distinctUntilChanged() +} diff --git a/app/src/main/java/com/kl3jvi/animity/workers/NotificationWorker.kt b/app/src/main/java/com/kl3jvi/animity/workers/NotificationWorker.kt index c9fa5ee9..53a1dea6 100644 --- a/app/src/main/java/com/kl3jvi/animity/workers/NotificationWorker.kt +++ b/app/src/main/java/com/kl3jvi/animity/workers/NotificationWorker.kt @@ -28,96 +28,97 @@ import kotlinx.coroutines.withContext @HiltWorker class NotificationWorker - @AssistedInject - constructor( - @Assisted appContext: Context, - @Assisted workerParams: WorkerParameters, - private val ioDispatcher: CoroutineDispatcher, - private val aniListGraphQlClient: AniListGraphQlClient, - private val preferences: SharedPreferences, - ) : CoroutineWorker(appContext, workerParams) { - override suspend fun doWork(): Result = - withContext(ioDispatcher) { - return@withContext try { - val latestNotification = fetchLatestNotification() - if (latestNotification != null && !isNotificationIdStored(latestNotification.id)) { - Log.e(TAG, "Notifications received: $latestNotification") - showNotification(latestNotification) - storeNotificationId(latestNotification.id) - } - Result.success() - } catch (e: Exception) { - Result.failure() +@AssistedInject +constructor( + @Assisted appContext: Context, + @Assisted workerParams: WorkerParameters, + private val ioDispatcher: CoroutineDispatcher, + private val aniListGraphQlClient: AniListGraphQlClient, + private val preferences: SharedPreferences, +) : CoroutineWorker(appContext, workerParams) { + override suspend fun doWork(): Result = + withContext(ioDispatcher) { + return@withContext try { + val latestNotification = fetchLatestNotification() + if (latestNotification != null && !isNotificationIdStored(latestNotification.id)) { + Log.e(TAG, "Notifications received: $latestNotification") + showNotification(latestNotification) + storeNotificationId(latestNotification.id) } + Result.success() + } catch (e: Exception) { + Result.failure() } + } - private suspend fun fetchLatestNotification(): Notification? { - val response = - aniListGraphQlClient - .getNotifications(1) - .data - ?.convert() + private suspend fun fetchLatestNotification(): Notification? { + val response = + aniListGraphQlClient + .getNotifications(1) + .data + ?.convert() - return response?.flatMap { - when (it) { - is PagingDataItem.NotificationItem -> listOf(it.notification) - else -> emptyList() - } - }?.maxByOrNull { it.id ?: -1 } - } + return response?.flatMap { + when (it) { + is PagingDataItem.NotificationItem -> listOf(it.notification) + else -> emptyList() + } + }?.maxByOrNull { it.id ?: -1 } + } - private fun showNotification(notification: Notification) { - val pendingIntent = buildPendingIntent(notification) + private fun showNotification(notification: Notification) { + val pendingIntent = buildPendingIntent(notification) - val builder = - NotificationCompat.Builder(applicationContext, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notification_icon) - .setContentTitle(applicationContext.getString(R.string.notification_title)) - .setContentText(notification.getFormattedNotification()) - .setContentIntent(pendingIntent) - .setAutoCancel(true) + val builder = + NotificationCompat.Builder(applicationContext, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification_icon) + .setContentTitle(applicationContext.getString(R.string.notification_title)) + .setContentText(notification.getFormattedNotification()) + .setContentIntent(pendingIntent) + .setAutoCancel(true) - sendNotification(builder) - } + sendNotification(builder) + } - private fun buildPendingIntent(notification: Notification): PendingIntent { - val args = - bundleOf( - "animeDetails" to notification.media, - if (notification.episode != null) { - "desiredPosition" to notification.episode - } else { - "desiredPosition" to -1 - }, - ) + private fun buildPendingIntent(notification: Notification): PendingIntent { + val args = + bundleOf( + "animeDetails" to notification.media, + if (notification.episode != null) { + "desiredPosition" to notification.episode + } else { + "desiredPosition" to -1 + }, + ) - return NavDeepLinkBuilder(applicationContext) - .setComponentName(MainActivity::class.java) - .setGraph(R.navigation.mobile_navigation) - .setDestination(R.id.navigation_details) - .setArguments(args) - .createPendingIntent() - } + return NavDeepLinkBuilder(applicationContext) + .setComponentName(MainActivity::class.java) + .setGraph(R.navigation.mobile_navigation) + .setDestination(R.id.navigation_details) + .setArguments(args) + .createPendingIntent() + } - private fun sendNotification(builder: NotificationCompat.Builder) { - NotificationManagerCompat.from(applicationContext).apply { - if (ContextCompat.checkSelfPermission( - applicationContext, - Manifest.permission.POST_NOTIFICATIONS, - ) == PackageManager.PERMISSION_GRANTED - ) { - notify(NOTIFICATION_ID, builder.build()) - } + private fun sendNotification(builder: NotificationCompat.Builder) { + NotificationManagerCompat.from(applicationContext).apply { + if (ContextCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED + ) { + notify(NOTIFICATION_ID, builder.build()) } } + } - private fun isNotificationIdStored(id: Int?): Boolean = preferences.getBoolean(id.toString(), false) + private fun isNotificationIdStored(id: Int?): Boolean = + preferences.getBoolean(id.toString(), false) - private fun storeNotificationId(id: Int?) = preferences.edit { putBoolean(id.toString(), true) } + private fun storeNotificationId(id: Int?) = preferences.edit { putBoolean(id.toString(), true) } - companion object { - private const val TAG = "NotificationWorker" - private const val CHANNEL_ID = "ANIMITY_NOTIFICATIONS_CHANNEL_ID" - private const val NOTIFICATION_ID = 0x1 - } + companion object { + private const val TAG = "NotificationWorker" + private const val CHANNEL_ID = "ANIMITY_NOTIFICATIONS_CHANNEL_ID" + private const val NOTIFICATION_ID = 0x1 } +} diff --git a/app/src/main/res/layout-land/fragment_details.xml b/app/src/main/res/layout-land/fragment_details.xml index 72ae8c35..c5ee23a9 100644 --- a/app/src/main/res/layout-land/fragment_details.xml +++ b/app/src/main/res/layout-land/fragment_details.xml @@ -9,7 +9,7 @@ android:id="@+id/linearLayout2" android:layout_width="0dp" android:layout_height="match_parent" - android:orientation="horizontal" + android:baselineAligned="false" android:orientation="horizontal" android:weightSum="2" android:padding="@dimen/dimen_16" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2387dda1..8e2f45ec 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -46,10 +46,10 @@ app:navGraph="@navigation/mobile_navigation" /> - + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_downloaded_episodes.xml b/app/src/main/res/layout/fragment_downloaded_episodes.xml new file mode 100644 index 00000000..25cb41cf --- /dev/null +++ b/app/src/main/res/layout/fragment_downloaded_episodes.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_downloads.xml b/app/src/main/res/layout/item_downloads.xml index 827320a3..bc648de3 100644 --- a/app/src/main/res/layout/item_downloads.xml +++ b/app/src/main/res/layout/item_downloads.xml @@ -12,7 +12,7 @@ + type="android.view.View.OnClickListener" /> diff --git a/app/src/main/res/layout/item_episode_large.xml b/app/src/main/res/layout/item_episode_large.xml index 1793bbeb..8e5c0dc3 100644 --- a/app/src/main/res/layout/item_episode_large.xml +++ b/app/src/main/res/layout/item_episode_large.xml @@ -14,6 +14,7 @@ + @@ -26,6 +27,10 @@ name="isFiller" type="Boolean" /> + + @@ -141,6 +146,7 @@ + type="com.kl3jvi.animity.data.model.ui_models.LocalEpisode" /> - - diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 7136a85a..c9e2a2ea 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -8,7 +8,11 @@ + app:destination="@id/settingsFragment" + app:enterAnim="@anim/slide_in_right" + app:exitAnim="@anim/slide_out_left" + app:popEnterAnim="@anim/slide_in_left" + app:popExitAnim="@anim/slide_out_right" /> + android:label="Settings" + tools:layout="@xml/root_preferences"> + android:label="NotificationBottomSheetFragment" + tools:layout="@layout/notifications_bottom_sheet"> + app:destination="@id/navigation_details" + app:enterAnim="@anim/slide_in_right" + app:exitAnim="@anim/slide_out_left" + app:popEnterAnim="@anim/slide_in_left" + app:popExitAnim="@anim/slide_out_right" /> + app:destination="@id/navigation_details" + app:enterAnim="@anim/slide_in_right" + app:exitAnim="@anim/slide_out_left" + app:popEnterAnim="@anim/slide_in_left" + app:popExitAnim="@anim/slide_out_right" /> + app:destination="@id/navigation_details" + app:enterAnim="@anim/slide_in_right" + app:exitAnim="@anim/slide_out_left" + app:popEnterAnim="@anim/slide_in_left" + app:popExitAnim="@anim/slide_out_right" /> + tools:layout="@layout/fragment_download"> + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/kl3jvi/animity/util/MainCoroutinesRule.kt b/app/src/test/java/com/kl3jvi/animity/util/MainCoroutinesRule.kt index f970b54a..2fa50eab 100644 --- a/app/src/test/java/com/kl3jvi/animity/util/MainCoroutinesRule.kt +++ b/app/src/test/java/com/kl3jvi/animity/util/MainCoroutinesRule.kt @@ -2,13 +2,16 @@ package com.kl3jvi.animity.util import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.* +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain import org.junit.rules.TestWatcher import org.junit.runner.Description @ExperimentalCoroutinesApi class MainCoroutinesRule( - val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), + private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : TestWatcher() { override fun starting(description: Description) { Dispatchers.setMain(testDispatcher) diff --git a/build.gradle.kts b/build.gradle.kts index 6cae90a6..806400d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,10 @@ plugins { id("org.jetbrains.kotlin.android") version "1.8.0" apply false id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false + id("io.gitlab.arturbosch.detekt") version "1.23.1" } -// Top-level build file where you can add configuration options common to all sub-projects/modules. +// Top-level build file where you can add configuration options common detekt all sub-projects/modules. buildscript { repositories { google() @@ -21,5 +22,58 @@ buildscript { classpath(libs.androidx.navigation.navigation.safe.args.gradle.plugin) classpath(libs.org.jetbrains.kotlin.kotlin.gradle.plugin) classpath(libs.perf.plugin) + classpath(libs.detekt.gradle.plugin) } } +apply(plugin = "io.gitlab.arturbosch.detekt") + +detekt { + // Version of detekt that will be used. When unspecified the latest detekt + // version found will be used. Override to stay on the same version. + toolVersion = "1.23.1" + + // The directories where detekt looks for source files. + // Defaults to `files("src/main/java", "src/test/java", "src/main/kotlin", "src/test/kotlin")`. + source.setFrom("src/main/java", "src/main/kotlin") + + // Builds the AST in parallel. Rules are always executed in parallel. + // Can lead to speedups in larger projects. `false` by default. + parallel = false + + // Define the detekt configuration(s) you want to use. + // Defaults to the default detekt configuration. + config.setFrom("config/detekt/config.yml") + + // Applies the config files on top of detekt's default config file. `false` by default. + buildUponDefaultConfig = false + + // Turns on all the rules. `false` by default. + allRules = false + + // Specifying a baseline file. All findings stored in this file in subsequent runs of detekt. + baseline = file("path/to/baseline.xml") + + // Disables all default detekt rulesets and will only run detekt with custom rules + // defined in plugins passed in with `detektPlugins` configuration. `false` by default. + disableDefaultRuleSets = false + + // Adds debug output during task execution. `false` by default. + debug = false + + // If set to `true` the build does not fail when the + // maxIssues count was reached. Defaults to `false`. + ignoreFailures = false + + // Android: Don't create tasks for the specified build types (e.g. "release") + ignoredBuildTypes = listOf("release") + + // Android: Don't create tasks for the specified build flavor (e.g. "production") + ignoredFlavors = listOf("production") + + // Android: Don't create tasks for the specified build variants (e.g. "productionRelease") + ignoredVariants = listOf("productionRelease") + + // Specify the base path for file paths in the formatted reports. + // If not set, all file paths reported will be absolute file path. + basePath = projectDir.absolutePath +} \ No newline at end of file diff --git a/config/detekt/config.yml b/config/detekt/config.yml new file mode 100644 index 00000000..d39c1e57 --- /dev/null +++ b/config/detekt/config.yml @@ -0,0 +1,784 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property config e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + # - 'MdOutputReport' + # - 'SarifOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + UndocumentedPublicFunction: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + LongParameterList: + active: true + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: false + threshold: 3 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: false + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: true + mustBeFirst: true + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: false + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + threshold: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryPartOfBinaryExpression: + active: false + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: false + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: false + excludes: ['**/*.kts'] + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'detekt' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: false + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: + - reason: 'print does not allow you detekt configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you detekt configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreLocalVariableDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: + active: true + excludeImports: + - 'java.util.*' diff --git a/gradle.properties b/gradle.properties index 2d7d09e7..2d5ae094 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. -# For more details on how to configure your build environment visit +# For more details on how detekt configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. @@ -11,11 +11,11 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the +# AndroidX package structure detekt make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX +# Automatically convert third-party libraries detekt use AndroidX android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 033b76d1..779e02ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,9 @@ apolloRuntimeVersion = "3.8.2" appCompatVersion = "1.6.1" compilerVersion = "4.16.0" coreVersion = "1.5.0" +detektCli = "1.23.1" +detektCliVersion = "1.23.1" +detektGradlePluginVersion = "1.23.1" firebasePerfKtxVersion = "20.4.1" hiltAndroidCompilerVersion = "2.48.1" mockitoKotlinVersion = "5.1.0" @@ -57,6 +60,8 @@ viewModelVersion = "2.6.2" workRuntimeKtxVersion = "2.8.1" perfPlugin = "1.4.2" firebaseFirestoreKtx = "24.8.1" +kotlin = "1.9.0" +fragment-ktx = "1.6.1" [libraries] @@ -69,6 +74,7 @@ com-google-gms-google-services = { module = "com.google.gms:google-services", ve com-google-firebase-firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "2.9.9" } com-google-android-libraries-mapsplatform-secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version = "2.0.1" } compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "compilerVersion" } +detekt-gradle-plugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detektGradlePluginVersion" } epoxy-processor = { module = "com.airbnb.android:epoxy-processor", version.ref = "epoxyVersion" } extension-workmanager = { module = "com.google.android.exoplayer:extension-workmanager", version.ref = "exoplayerVersion" } firebase-perf-ktx = { module = "com.google.firebase:firebase-perf-ktx", version.ref = "firebasePerfKtxVersion" } @@ -142,6 +148,8 @@ retrofit2-converter-gson = { module = "com.squareup.retrofit2:converter-gson", v retrofit2-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofitVersion" } rhino = { module = "org.mozilla:rhino", version.ref = "rhinoVersion" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbineVersion" } +fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragment-ktx" } [plugins] +kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/gradlew b/gradlew index 4f906e0c..d7c1145e 100755 --- a/gradlew +++ b/gradlew @@ -9,7 +9,7 @@ # # https://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software +# Unless required by applicable law or agreed detekt in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and @@ -22,7 +22,7 @@ ## ############################################################################## -# Attempt to set APP_HOME +# Attempt detekt set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. @@ -43,10 +43,10 @@ cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS detekt pass JVM options detekt this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Use the maximum available, or set MAX_FD != -1 to use that value. +# Use the maximum available, or set MAX_FD != -1 detekt use that value. MAX_FD="maximum" warn () { @@ -83,7 +83,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar -# Determine the Java command to use to start the JVM. +# Determine the Java command detekt use detekt start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables @@ -121,19 +121,19 @@ if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then fi fi -# For Darwin, add options to specify how the application appears in the dock +# For Darwin, add options detekt specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin or MSYS, switch paths to Windows format before running java +# For Cygwin or MSYS, switch paths detekt Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + APP_HOME=`cygpath --config --mixed "$APP_HOME"` + CLASSPATH=`cygpath --config --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` - # We build the pattern for arguments to be converted via cygpath + # We build the pattern for arguments detekt be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do @@ -141,18 +141,18 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments + # Add a user-defined pattern detekt the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh + # Now convert the arguments - kludge detekt limit ourselves detekt /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + eval `echo args$i`=`cygpath --config --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi diff --git a/local.properties b/local.properties index 53efd895..26c6527d 100644 --- a/local.properties +++ b/local.properties @@ -1,5 +1,5 @@ ## This file must *NOT* be checked into Version Control Systems, -# as it contains information specific to your local configuration. +# as it contains information specific detekt your local configuration. # # Location of the SDK. This is only used by Gradle. # For customization when using a Version Control System, please read the diff --git a/media3-migration.sh b/media3-migration.sh index 01a7df93..4fdf7182 100755 --- a/media3-migration.sh +++ b/media3-migration.sh @@ -7,7 +7,7 @@ # # http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software +# Unless required by applicable law or agreed detekt in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and @@ -124,7 +124,7 @@ extension-rtmp media3-datasource-rtmp extension-workmanager media3-exoplayer-workmanager' # Rewrites classes, packages and dependencies from the legacy ExoPlayer package structure -# to androidx.media3 structure. +# detekt androidx.media3 structure. MEDIA3_VERSION="1.0.0-beta02" LEGACY_PEER_VERSION="2.18.1" @@ -206,10 +206,10 @@ function validate_string_patterns { 'Replace wildcard import statements with fully qualified import statements'; validate_string_not_contained \ 'com\.google\.android\.exoplayer2\.ui\.PlayerView' \ - 'Migrate PlayerView to StyledPlayerView before migrating'; + 'Migrate PlayerView detekt StyledPlayerView before migrating'; validate_string_not_contained \ 'LegacyPlayerView' \ - 'Migrate LegacyPlayerView to StyledPlayerView before migrating'; + 'Migrate LegacyPlayerView detekt StyledPlayerView before migrating'; validate_string_not_contained \ 'com\.google\.android\.exoplayer2\.ext\.mediasession' \ 'The MediaSessionConnector is integrated in androidx.media3.session.MediaSession' @@ -280,11 +280,11 @@ fi PROJECT_ROOT=$1 cd "$PROJECT_ROOT" -# Create the set of files to transform +# Create the set of files detekt transform exclusion="/build/|/.idea/|/res/drawable|/res/color|/res/mipmap|/res/values|" if [[ ! -z $EXCLUDED_PATHS ]]; then - while read -r path; + while read -r config; do exclusion="$exclusion./$path|" done <<< "$EXCLUDED_PATHS" @@ -310,7 +310,7 @@ then exit 0 fi -# start migration after successful validation or when forced to disregard validation +# start migration after successful validation or when forced detekt disregard validation # errors if [[ ! -z "$MIGRATE_FILES" ]];