From df2867c7dbaf6744737860f1b9d5b590fb2de1f9 Mon Sep 17 00:00:00 2001 From: rebelonion <87634197+rebelonion@users.noreply.github.com> Date: Fri, 17 May 2024 10:11:42 -0500 Subject: [PATCH 1/6] feat: subscriptions in notifications --- .../connections/anilist/api/Notification.kt | 3 +- .../comment/CommentNotificationTask.kt | 2 +- .../notifications/comment/CommentStore.kt | 2 - .../SubscriptionNotificationTask.kt | 22 +++++++ .../subscription/SubscriptionStore.kt | 16 +++++ .../profile/activity/ActivityItemBuilder.kt | 4 ++ .../profile/activity/NotificationActivity.kt | 59 ++++++++++++++----- .../profile/activity/NotificationItem.kt | 14 +++++ .../dantotsu/settings/saving/Preferences.kt | 2 + 9 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/ani/dantotsu/notifications/subscription/SubscriptionStore.kt diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Notification.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Notification.kt index e4338a37..ac404203 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Notification.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Notification.kt @@ -26,7 +26,8 @@ enum class NotificationType(val value: String) { //custom COMMENT_REPLY("COMMENT_REPLY"), COMMENT_WARNING("COMMENT_WARNING"), - DANTOTSU_UPDATE("DANTOTSU_UPDATE"); + DANTOTSU_UPDATE("DANTOTSU_UPDATE"), + SUBSCRIPTION("SUBSCRIPTION"); fun toFormattedString(): String { return this.value.replace("_", " ").lowercase(Locale.ROOT) diff --git a/app/src/main/java/ani/dantotsu/notifications/comment/CommentNotificationTask.kt b/app/src/main/java/ani/dantotsu/notifications/comment/CommentNotificationTask.kt index eb7837d3..97cbcc0a 100644 --- a/app/src/main/java/ani/dantotsu/notifications/comment/CommentNotificationTask.kt +++ b/app/src/main/java/ani/dantotsu/notifications/comment/CommentNotificationTask.kt @@ -188,7 +188,7 @@ class CommentNotificationTask : Task { null ) ?: listOf() val newStore = notificationStore.toMutableList() - if (newStore.size > 10) { + if (newStore.size > 30) { newStore.remove(newStore.minByOrNull { it.time }) } if (newStore.any { it.content == notification.content }) { diff --git a/app/src/main/java/ani/dantotsu/notifications/comment/CommentStore.kt b/app/src/main/java/ani/dantotsu/notifications/comment/CommentStore.kt index 6024aa71..e9381d45 100644 --- a/app/src/main/java/ani/dantotsu/notifications/comment/CommentStore.kt +++ b/app/src/main/java/ani/dantotsu/notifications/comment/CommentStore.kt @@ -13,8 +13,6 @@ data class CommentStore( val time: Long = System.currentTimeMillis(), ) : java.io.Serializable { companion object { - - @Suppress("INAPPROPRIATE_CONST_NAME") private const val serialVersionUID = 2L } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/notifications/subscription/SubscriptionNotificationTask.kt b/app/src/main/java/ani/dantotsu/notifications/subscription/SubscriptionNotificationTask.kt index 83345013..7ea81ae8 100644 --- a/app/src/main/java/ani/dantotsu/notifications/subscription/SubscriptionNotificationTask.kt +++ b/app/src/main/java/ani/dantotsu/notifications/subscription/SubscriptionNotificationTask.kt @@ -118,6 +118,15 @@ class SubscriptionNotificationTask : Task { if (ep != null) ep.number + " " + context.getString(R.string.just_released) to null else null } ?: return@map + addSubscriptionToStore( + SubscriptionStore( + media.name, + text.first, + media.id + ) + ) + PrefManager.setVal(PrefName.UnreadCommentNotifications, + PrefManager.getVal(PrefName.UnreadCommentNotifications) + 1) val notification = createNotification( context.applicationContext, media, @@ -219,4 +228,17 @@ class SubscriptionNotificationTask : Task { } ) } + + private fun addSubscriptionToStore(notification: SubscriptionStore) { + val notificationStore = PrefManager.getNullableVal>( + PrefName.SubscriptionNotificationStore, + null + ) ?: listOf() + val newStore = notificationStore.toMutableList() + if (newStore.size >= 100) { + newStore.remove(newStore.minByOrNull { it.time }) + } + newStore.add(notification) + PrefManager.setVal(PrefName.SubscriptionNotificationStore, newStore) + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/notifications/subscription/SubscriptionStore.kt b/app/src/main/java/ani/dantotsu/notifications/subscription/SubscriptionStore.kt new file mode 100644 index 00000000..dfb910f5 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/notifications/subscription/SubscriptionStore.kt @@ -0,0 +1,16 @@ +package ani.dantotsu.notifications.subscription + +import kotlinx.serialization.Serializable + +@Serializable +data class SubscriptionStore( + val title: String, + val content: String, + val mediaId: Int, + val type: String = "SUBSCRIPTION", + val time: Long = System.currentTimeMillis(), +) : java.io.Serializable { + companion object { + private const val serialVersionUID = 1L + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/profile/activity/ActivityItemBuilder.kt b/app/src/main/java/ani/dantotsu/profile/activity/ActivityItemBuilder.kt index 61840c32..749716ee 100644 --- a/app/src/main/java/ani/dantotsu/profile/activity/ActivityItemBuilder.kt +++ b/app/src/main/java/ani/dantotsu/profile/activity/ActivityItemBuilder.kt @@ -92,6 +92,10 @@ class ActivityItemBuilder { NotificationType.DANTOTSU_UPDATE -> { notification.context ?: "You should not see this" } + + NotificationType.SUBSCRIPTION -> { + notification.context ?: "You should not see this" + } } } diff --git a/app/src/main/java/ani/dantotsu/profile/activity/NotificationActivity.kt b/app/src/main/java/ani/dantotsu/profile/activity/NotificationActivity.kt index ae27260b..30b0c525 100644 --- a/app/src/main/java/ani/dantotsu/profile/activity/NotificationActivity.kt +++ b/app/src/main/java/ani/dantotsu/profile/activity/NotificationActivity.kt @@ -28,6 +28,7 @@ import ani.dantotsu.initActivity import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.navBarHeight import ani.dantotsu.notifications.comment.CommentStore +import ani.dantotsu.notifications.subscription.SubscriptionStore import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName @@ -41,6 +42,8 @@ import kotlinx.coroutines.withContext class NotificationActivity : AppCompatActivity() { private lateinit var binding: ActivityFollowBinding + private lateinit var commentStore: List + private lateinit var subscriptionStore: List private var adapter: GroupieAdapter = GroupieAdapter() private var notificationList: List = emptyList() val filters = ArrayList() @@ -70,6 +73,15 @@ class NotificationActivity : AppCompatActivity() { onBackPressedDispatcher.onBackPressed() } binding.listProgressBar.visibility = ViewGroup.VISIBLE + commentStore = PrefManager.getNullableVal>( + PrefName.CommentNotificationStore, + null + ) ?: listOf() + subscriptionStore = PrefManager.getNullableVal>( + PrefName.SubscriptionNotificationStore, + null + ) ?: listOf() + binding.followFilterButton.setOnClickListener { val dialogView = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null) val checkboxContainer = dialogView.findViewById(R.id.checkboxContainer) @@ -193,22 +205,39 @@ class NotificationActivity : AppCompatActivity() { notifications }.toMutableList() } - if (activityId == -1 && currentPage == 1) { - val commentStore = PrefManager.getNullableVal>( - PrefName.CommentNotificationStore, - null - ) ?: listOf() + if (activityId == -1) { + val furthestTime = newNotifications.minOfOrNull { it.createdAt } ?: 0 commentStore.forEach { - val notification = Notification( - it.type.toString(), - System.currentTimeMillis().toInt(), - commentId = it.commentId, - notificationType = it.type.toString(), - mediaId = it.mediaId, - context = it.title + "\n" + it.content, - createdAt = (it.time / 1000L).toInt(), - ) - newNotifications += notification + if ((it.time > furthestTime * 1000L || !hasNextPage) && notificationList.none { notification -> + notification.commentId == it.commentId && notification.createdAt == (it.time / 1000L).toInt() + }) { + val notification = Notification( + it.type.toString(), + System.currentTimeMillis().toInt(), + commentId = it.commentId, + notificationType = it.type.toString(), + mediaId = it.mediaId, + context = it.title + "\n" + it.content, + createdAt = (it.time / 1000L).toInt(), + ) + newNotifications += notification + } + } + subscriptionStore.forEach { + if ((it.time > furthestTime * 1000L || !hasNextPage) && notificationList.none { notification -> + notification.mediaId == it.mediaId && notification.createdAt == (it.time / 1000L).toInt() + }) { + val notification = Notification( + it.type, + System.currentTimeMillis().toInt(), + commentId = it.mediaId, + mediaId = it.mediaId, + notificationType = it.type, + context = it.content, + createdAt = (it.time / 1000L).toInt(), + ) + newNotifications += notification + } } newNotifications.sortByDescending { it.createdAt } } diff --git a/app/src/main/java/ani/dantotsu/profile/activity/NotificationItem.kt b/app/src/main/java/ani/dantotsu/profile/activity/NotificationItem.kt index 139d3200..23224184 100644 --- a/app/src/main/java/ani/dantotsu/profile/activity/NotificationItem.kt +++ b/app/src/main/java/ani/dantotsu/profile/activity/NotificationItem.kt @@ -332,6 +332,20 @@ class NotificationItem( NotificationType.DANTOTSU_UPDATE -> { image(user = true) } + + NotificationType.SUBSCRIPTION -> { + image(user = true, commentNotification = true) + binding.notificationCoverUser.setOnClickListener { + clickCallback( + notification.mediaId ?: 0, null, NotificationClickType.MEDIA + ) + } + binding.notificationBannerImage.setOnClickListener { + clickCallback( + notification.mediaId ?: 0, null, NotificationClickType.MEDIA + ) + } + } } } diff --git a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt index 0e275cf7..df6ed929 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt @@ -4,6 +4,7 @@ import android.graphics.Color import ani.dantotsu.connections.comments.AuthResponse import ani.dantotsu.connections.mal.MAL import ani.dantotsu.notifications.comment.CommentStore +import ani.dantotsu.notifications.subscription.SubscriptionStore import ani.dantotsu.settings.saving.internal.Location import ani.dantotsu.settings.saving.internal.Pref @@ -185,6 +186,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files LogToFile(Pref(Location.Irrelevant, Boolean::class, false)), RecentGlobalNotification(Pref(Location.Irrelevant, Int::class, 0)), CommentNotificationStore(Pref(Location.Irrelevant, List::class, listOf())), + SubscriptionNotificationStore(Pref(Location.Irrelevant, List::class, listOf())), UnreadCommentNotifications(Pref(Location.Irrelevant, Int::class, 0)), DownloadsDir(Pref(Location.Irrelevant, String::class, "")), RefreshStatus(Pref(Location.Irrelevant, Boolean::class, false)), From 949bcc418ac130624f1ea7d24384948ee1dffb3d Mon Sep 17 00:00:00 2001 From: rebelonion <87634197+rebelonion@users.noreply.github.com> Date: Sat, 18 May 2024 11:43:30 -0500 Subject: [PATCH 2/6] fix: some error checking --- .../java/ani/dantotsu/addons/AddonLoader.kt | 63 ++++++++++++++----- .../extension/util/ExtensionInstaller.kt | 14 ++++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 62 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/addons/AddonLoader.kt b/app/src/main/java/ani/dantotsu/addons/AddonLoader.kt index 25681b94..f2dc4488 100644 --- a/app/src/main/java/ani/dantotsu/addons/AddonLoader.kt +++ b/app/src/main/java/ani/dantotsu/addons/AddonLoader.kt @@ -21,6 +21,20 @@ import eu.kanade.tachiyomi.util.system.getApplicationIcon class AddonLoader { companion object { + + /** + * Load an extension from a package name with a specific class name + * @param context the context + * @param packageName the package name of the extension + * @param type the type of extension + * @return the loaded extension + * @throws IllegalStateException if the extension is not of the correct type + * @throws ClassNotFoundException if the extension class is not found + * @throws NoClassDefFoundError if the extension class is not found + * @throws Exception if any other error occurs + * @throws PackageManager.NameNotFoundException if the package is not found + * @throws IllegalStateException if the extension is not found + */ fun loadExtension( context: Context, packageName: String, @@ -70,11 +84,11 @@ class AddonLoader { val loadedClass = try { Class.forName(className, false, classLoader) } catch (e: ClassNotFoundException) { - Logger.log("Extension load error: $extName ($className)") + Logger.log("ClassNotFoundException load error: $extName ($className)") Logger.log(e) throw e } catch (e: NoClassDefFoundError) { - Logger.log("Extension load error: $extName ($className)") + Logger.log("NoClassDefFoundError load error: $extName ($className)") Logger.log(e) throw e } catch (e: Exception) { @@ -117,24 +131,43 @@ class AddonLoader { } } + /** + * Load an extension from a package name (class is determined by type) + * @param context the context + * @param packageName the package name of the extension + * @param type the type of extension + * @return the loaded extension + */ fun loadFromPkgName(context: Context, packageName: String, type: AddonType): LoadResult? { - return when (type) { - AddonType.TORRENT -> loadExtension( - context, - packageName, - TorrentAddonManager.TORRENT_CLASS, - type - ) + return try { + when (type) { + AddonType.TORRENT -> loadExtension( + context, + packageName, + TorrentAddonManager.TORRENT_CLASS, + type + ) - AddonType.DOWNLOAD -> loadExtension( - context, - packageName, - DownloadAddonManager.DOWNLOAD_CLASS, - type - ) + AddonType.DOWNLOAD -> loadExtension( + context, + packageName, + DownloadAddonManager.DOWNLOAD_CLASS, + type + ) + } + } catch (e: Exception) { + Logger.log("Error loading extension from package name: $packageName") + Logger.log(e) + null } } + /** + * Check if a package is an extension by comparing the package name + * @param type the type of extension + * @param pkgInfo the package info + * @return true if the package is an extension + */ private fun isPackageAnExtension(type: String, pkgInfo: PackageInfo): Boolean { return pkgInfo.packageName.equals(type) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index a2f9e83d..13106131 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.extension.util import android.app.DownloadManager +import android.app.ForegroundServiceStartNotAllowedException import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -11,9 +12,11 @@ import android.os.Environment import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.net.toUri +import ani.dantotsu.R import ani.dantotsu.media.AddonType import ani.dantotsu.media.MediaType import ani.dantotsu.media.Type +import ani.dantotsu.toast import ani.dantotsu.util.Logger import com.jakewharton.rxrelay.PublishRelay import eu.kanade.domain.base.BasePreferences @@ -160,7 +163,16 @@ class ExtensionInstaller(private val context: Context) { else -> { val intent = ExtensionInstallService.getIntent(context, type, downloadId, uri, installer) - ContextCompat.startForegroundService(context, intent) + try { + ContextCompat.startForegroundService(context, intent) + } catch (e: RuntimeException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) { + toast(context.getString(R.string.error_msg, context.getString(R.string.foreground_service_not_allowed))) + } else { + toast(context.getString(R.string.error_msg, e.message)) + } + Logger.log(e) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14e4e115..5640e828 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -995,4 +995,5 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc View and edit all your subscriptions Subscriptions Subscription Deleted + Cannot install when app is in background From bfa847130e404c58d69ea7b1e4228c996688cf9d Mon Sep 17 00:00:00 2001 From: aayush262 Date: Sat, 18 May 2024 23:49:43 +0530 Subject: [PATCH 3/6] fix: idr --- .../connections/anilist/AnilistQueries.kt | 2 +- .../connections/github/Contributors.kt | 6 +- .../java/ani/dantotsu/home/status/Stories.kt | 81 +++++++++++-------- .../ani/dantotsu/settings/SubscriptionItem.kt | 4 +- app/src/main/res/layout/item_follower.xml | 5 +- .../main/res/layout/item_search_history.xml | 2 +- app/src/main/res/layout/item_subscription.xml | 25 +++++- 7 files changed, 81 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt index e98966e6..c77cf105 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt @@ -1505,7 +1505,7 @@ Page(page:$page,perPage:50) { return author } - suspend fun getReviews(mediaId: Int, page: Int = 1, sort: String = "UPDATED_AT_DESC"): Query.ReviewsResponse? { + suspend fun getReviews(mediaId: Int, page: Int = 1, sort: String = "CREATED_AT_DESC"): Query.ReviewsResponse? { return executeQuery( """{Page(page:$page,perPage:10){pageInfo{currentPage,hasNextPage,total}reviews(mediaId:$mediaId,sort:$sort){id,mediaId,mediaType,summary,body(asHtml:true)rating,ratingAmount,userRating,score,private,siteUrl,createdAt,updatedAt,user{id,name,bannerImage avatar{medium,large}}}}}""", force = true diff --git a/app/src/main/java/ani/dantotsu/connections/github/Contributors.kt b/app/src/main/java/ani/dantotsu/connections/github/Contributors.kt index b8863310..337e4c49 100644 --- a/app/src/main/java/ani/dantotsu/connections/github/Contributors.kt +++ b/app/src/main/java/ani/dantotsu/connections/github/Contributors.kt @@ -84,15 +84,15 @@ class Contributors { "https://anilist.co/user/6244220" ), Developer( - "Zaidsenior", + "Ziadsenior", "https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6049773-8cjYeUOFUguv.jpg", - "Comment Moderator", + "Comment Moderator and Arabic Translator", "https://anilist.co/user/6049773" ), Developer( "hastsu", "https://cdn.discordapp.com/avatars/602422545077108749/20b4a6efa4314550e4ed51cdbe4fef3d.webp?size=160", - "Comment Moderator", + "Comment Moderator and Arabic Translator", "https://anilist.co/user/6183359" ), ) diff --git a/app/src/main/java/ani/dantotsu/home/status/Stories.kt b/app/src/main/java/ani/dantotsu/home/status/Stories.kt index f0d3cf2b..2779eff5 100644 --- a/app/src/main/java/ani/dantotsu/home/status/Stories.kt +++ b/app/src/main/java/ani/dantotsu/home/status/Stories.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.Calendar import java.util.Locale +import kotlin.math.abs class Stories @JvmOverloads constructor( @@ -264,29 +265,44 @@ class Stories @JvmOverloads constructor( private var startClickTime = 0L + private var startX = 0f + private var startY = 0f + private var isLongPress = false + private val swipeThreshold = 100 override fun onTouch(view: View?, event: MotionEvent?): Boolean { val maxClickDuration = 200 when (event?.action) { MotionEvent.ACTION_DOWN -> { + startX = event.x + startY = event.y startClickTime = Calendar.getInstance().timeInMillis pause() + isLongPress = false + } + + MotionEvent.ACTION_MOVE -> { + val deltaX = event.x - startX + val deltaY = event.y - startY + if (!isLongPress && (abs(deltaX) > swipeThreshold || abs(deltaY) > swipeThreshold)) { + isLongPress = true + } } MotionEvent.ACTION_UP -> { val clickDuration = Calendar.getInstance().timeInMillis - startClickTime - if (clickDuration < maxClickDuration) { - //click occurred - view?.let { - if (it.id == R.id.leftTouchPanel) { - leftPanelTouch() - } else if (it.id == R.id.rightTouchPanel) { - rightPanelTouch() - } + if (clickDuration < maxClickDuration && !isLongPress) { + when (view?.id) { + R.id.leftTouchPanel -> leftPanelTouch() + R.id.rightTouchPanel -> rightPanelTouch() } } else { - //hold click occurred resume() } + val deltaX = event.x - startX + if (abs(deltaX) > swipeThreshold) { + if (deltaX > 0) onStoriesPrevious() + else onStoriesCompleted() + } } } return true @@ -359,16 +375,14 @@ class Stories @JvmOverloads constructor( ) } fun visible(isList: Boolean) { - val visible = if (isList) View.VISIBLE else View.GONE - val gone = if (isList) View.GONE else View.VISIBLE - binding.textActivity.visibility = gone - binding.textActivityContainer.visibility = gone - binding.infoText.visibility = visible - binding.coverImage.visibility = visible + binding.textActivity.isVisible = !isList + binding.textActivityContainer.isVisible = !isList + binding.infoText.isVisible = isList + binding.coverImage.isVisible = isList binding.infoText.visibility = if (isList) View.VISIBLE else View.INVISIBLE binding.infoText.text = "" - binding.contentImageViewKen.visibility = visible - binding.contentImageView.visibility = visible + binding.contentImageViewKen.isVisible = isList + binding.contentImageView.isVisible = isList } when (story.typename) { @@ -383,16 +397,15 @@ class Stories @JvmOverloads constructor( } } } ${story.progress ?: story.media?.title?.userPreferred} " + - if ( - story.status?.contains("completed") == false && - !story.status.contains("plans") && - !story.status.contains("repeating") - ) - { - "of ${story.media?.title?.userPreferred}" - } else { - "" - } + if ( + story.status?.contains("completed") == false && + !story.status.contains("plans") && + !story.status.contains("repeating") + ) { + "of ${story.media?.title?.userPreferred}" + } else { + "" + } binding.infoText.text = text val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations) blurImage( @@ -404,18 +417,16 @@ class Stories @JvmOverloads constructor( ContextCompat.startActivity( context, Intent(context, MediaDetailsActivity::class.java).putExtra( - "mediaId", - story.media?.id - ), + "mediaId", + story.media?.id + ), ActivityOptionsCompat.makeSceneTransitionAnimation( activity, binding.coverImage, ViewCompat.getTransitionName(binding.coverImage)!! ).toBundle() ) - } - } "TextActivity" -> { @@ -445,11 +456,13 @@ class Stories @JvmOverloads constructor( val likeColor = ContextCompat.getColor(context, R.color.yt_red) val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp) binding.activityRepliesContainer.setOnClickListener { - RepliesBottomDialog.newInstance(story.id).show(activity.supportFragmentManager, "replies") + RepliesBottomDialog.newInstance(story.id) + .show(activity.supportFragmentManager, "replies") } + binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor) binding.replyCount.text = story.replyCount.toString() binding.activityLikeCount.text = story.likeCount.toString() - binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor) + binding.activityLike.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp)) binding.activityLikeContainer.setOnClickListener { like() } diff --git a/app/src/main/java/ani/dantotsu/settings/SubscriptionItem.kt b/app/src/main/java/ani/dantotsu/settings/SubscriptionItem.kt index 6c6cf941..bb8c0396 100644 --- a/app/src/main/java/ani/dantotsu/settings/SubscriptionItem.kt +++ b/app/src/main/java/ani/dantotsu/settings/SubscriptionItem.kt @@ -5,6 +5,7 @@ import android.view.View import androidx.core.content.ContextCompat import ani.dantotsu.R import ani.dantotsu.databinding.ItemSubscriptionBinding +import ani.dantotsu.loadImage import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.notifications.subscription.SubscriptionHelper import com.xwray.groupie.GroupieAdapter @@ -25,7 +26,7 @@ class SubscriptionItem( else SubscriptionHelper.getMangaParser(media.id).name val mediaName = media.name - val showName = "$mediaName - $parserName" + val showName = "$mediaName ($parserName)" binding.subscriptionName.text = showName binding.root.setOnClickListener { ContextCompat.startActivity( @@ -36,6 +37,7 @@ class SubscriptionItem( null ) } + binding.subscriptionCover.loadImage(media.image) binding.deleteSubscription.setOnClickListener { SubscriptionHelper.deleteSubscription(id, true) adapter.remove(this) diff --git a/app/src/main/res/layout/item_follower.xml b/app/src/main/res/layout/item_follower.xml index 675e5451..6c236619 100644 --- a/app/src/main/res/layout/item_follower.xml +++ b/app/src/main/res/layout/item_follower.xml @@ -74,11 +74,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="start|center_vertical" - android:ellipsize="end" android:fontFamily="@font/poppins_semi_bold" + android:ellipsize="end" + android:alpha="0.75" android:maxLines="2" android:text="@string/lorem_ipsum" - android:textSize="14sp" + android:textSize="12sp" android:visibility="gone" /> diff --git a/app/src/main/res/layout/item_search_history.xml b/app/src/main/res/layout/item_search_history.xml index 82155f90..237ecbef 100644 --- a/app/src/main/res/layout/item_search_history.xml +++ b/app/src/main/res/layout/item_search_history.xml @@ -30,7 +30,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" - android:src="@drawable/ic_circle_cancel" + android:src="@drawable/ic_round_close_24" android:textSize="14sp" app:tint="?attr/colorOnBackground" tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/item_subscription.xml b/app/src/main/res/layout/item_subscription.xml index be8dd588..d6609cd6 100644 --- a/app/src/main/res/layout/item_subscription.xml +++ b/app/src/main/res/layout/item_subscription.xml @@ -8,14 +8,35 @@ android:layout_marginHorizontal="8dp" android:orientation="horizontal" tools:ignore="UseCompoundDrawables"> + + + + @@ -26,7 +47,7 @@ android:layout_gravity="center_vertical" android:layout_marginEnd="8dp" android:contentDescription="@string/delete" - android:src="@drawable/ic_circle_cancel" + android:src="@drawable/ic_round_close_24" app:tint="?attr/colorOnBackground" /> \ No newline at end of file From 114be6fe5a1ec67d7d075090e7dd24c06e8324b3 Mon Sep 17 00:00:00 2001 From: aayush262 Date: Sun, 19 May 2024 14:47:43 +0530 Subject: [PATCH 4/6] fix: review rework --- .../ani/dantotsu/home/status/CircleView.kt | 3 +- .../java/ani/dantotsu/home/status/Stories.kt | 2 +- .../java/ani/dantotsu/media/ReviewActivity.kt | 33 +--- .../java/ani/dantotsu/media/ReviewAdapter.kt | 129 +++++++++++++ .../main/res/layout/activity_review_view.xml | 1 + app/src/main/res/layout/item_developer.xml | 6 +- app/src/main/res/layout/item_reviews.xml | 182 ++++++++++++++++++ 7 files changed, 326 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/ani/dantotsu/media/ReviewAdapter.kt create mode 100644 app/src/main/res/layout/item_reviews.xml diff --git a/app/src/main/java/ani/dantotsu/home/status/CircleView.kt b/app/src/main/java/ani/dantotsu/home/status/CircleView.kt index 2a7c02d9..63986d83 100644 --- a/app/src/main/java/ani/dantotsu/home/status/CircleView.kt +++ b/app/src/main/java/ani/dantotsu/home/status/CircleView.kt @@ -12,13 +12,14 @@ import ani.dantotsu.getThemeColor class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs) { private var parts: Int = 3 - private var gapAngle: Float = 9f + private var gapAngle: Float = 12f private val path = Path() private var isUser = false private var booleanList = listOf() private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE strokeWidth = 6f + strokeCap = Paint.Cap.ROUND } @SuppressLint("DrawAllocation") diff --git a/app/src/main/java/ani/dantotsu/home/status/Stories.kt b/app/src/main/java/ani/dantotsu/home/status/Stories.kt index 2779eff5..7f2c60ec 100644 --- a/app/src/main/java/ani/dantotsu/home/status/Stories.kt +++ b/app/src/main/java/ani/dantotsu/home/status/Stories.kt @@ -462,7 +462,7 @@ class Stories @JvmOverloads constructor( binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor) binding.replyCount.text = story.replyCount.toString() binding.activityLikeCount.text = story.likeCount.toString() - binding.activityLike.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp)) + binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp)) binding.activityLikeContainer.setOnClickListener { like() } diff --git a/app/src/main/java/ani/dantotsu/media/ReviewActivity.kt b/app/src/main/java/ani/dantotsu/media/ReviewActivity.kt index b4631db4..8f3c3b12 100644 --- a/app/src/main/java/ani/dantotsu/media/ReviewActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/ReviewActivity.kt @@ -77,7 +77,7 @@ class ReviewActivity : AppCompatActivity() { binding.listBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() } lifecycleScope.launch(Dispatchers.IO) { - val response = Anilist.query.getReviews(mediaId) + val response = Anilist.query.getReviews(mediaId)?.data?.page withContext(Dispatchers.Main) { binding.listProgressBar.visibility = View.GONE binding.listRecyclerView.setOnTouchListener { _, event -> @@ -94,9 +94,9 @@ class ReviewActivity : AppCompatActivity() { } false } - currentPage = response?.data?.page?.pageInfo?.currentPage ?: 1 - hasNextPage = response?.data?.page?.pageInfo?.hasNextPage ?: false - response?.data?.page?.reviews?.let { + currentPage = response?.pageInfo?.currentPage ?: 1 + hasNextPage = response?.pageInfo?.hasNextPage ?: false + response?.reviews?.let { reviews.addAll(it) fillList() } @@ -122,29 +122,10 @@ class ReviewActivity : AppCompatActivity() { private fun fillList() { adapter.clear() reviews.forEach { - val username = it.user?.name ?: "Unknown" - val name = SpannableString(username + " - " + it.score) - //change the size of the score - name.setSpan( - android.text.style.RelativeSizeSpan(0.9f), - 0, - name.length, - android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - //give the text an underline - name.setSpan( - android.text.style.UnderlineSpan(), - username.length + 3, - name.length, - android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) adapter.add( - FollowerItem( - it.id, - name, - it.user?.avatar?.medium, - it.user?.bannerImage, - it.summary, + ReviewAdapter( + it, + this, this::onUserClick ) ) diff --git a/app/src/main/java/ani/dantotsu/media/ReviewAdapter.kt b/app/src/main/java/ani/dantotsu/media/ReviewAdapter.kt new file mode 100644 index 00000000..1f8034d3 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/media/ReviewAdapter.kt @@ -0,0 +1,129 @@ +package ani.dantotsu.media + + +import android.content.Context +import android.text.SpannableString +import android.view.View +import androidx.lifecycle.lifecycleScope +import ani.dantotsu.R +import ani.dantotsu.blurImage +import ani.dantotsu.connections.anilist.Anilist +import ani.dantotsu.connections.anilist.api.Query +import ani.dantotsu.databinding.ItemFollowerBinding +import ani.dantotsu.databinding.ItemReviewsBinding +import ani.dantotsu.getThemeColor +import ani.dantotsu.loadImage +import ani.dantotsu.profile.activity.ActivityItemBuilder +import ani.dantotsu.toast +import com.xwray.groupie.viewbinding.BindableItem +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ReviewAdapter( + private var review: Query.Review, + val context: ReviewActivity, + val clickCallback: (Int) -> Unit + +) : BindableItem() { + private lateinit var binding: ItemReviewsBinding + + override fun bind(viewBinding: ItemReviewsBinding, position: Int) { + binding = viewBinding + binding.reviewUserName.text = review.user?.name + binding.reviewUserAvatar.loadImage(review.user?.avatar?.medium) + binding.reviewText.text = review.summary + binding.reviewPostTime.text = ActivityItemBuilder.getDateTime(review.createdAt) + binding.reviewTag.text = "[${review.score}]" + binding.root.setOnClickListener { clickCallback(review.id) } + userVote(review.userRating) + enableVote() + binding.reviewTotalVotes.text = review.rating.toString() + } + + override fun getLayout(): Int { + return R.layout.item_reviews + } + + override fun initializeViewBinding(view: View): ItemReviewsBinding { + return ItemReviewsBinding.bind(view) + } + private fun userVote(type: String) { + when (type) { + "NO_VOTE" -> { + binding.reviewUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24) + binding.reviewDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24) + binding.reviewUpVote.alpha = 0.6f + binding.reviewDownVote.alpha = 0.6f + } + + "UP_VOTE" -> { + binding.reviewUpVote.setImageResource(R.drawable.ic_round_upvote_active_24) + binding.reviewDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24) + binding.reviewUpVote.alpha = 1f + binding.reviewDownVote.alpha = 0.6f + } + + "DOWN_VOTE" -> { + binding.reviewUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24) + binding.reviewDownVote.setImageResource(R.drawable.ic_round_upvote_active_24) + binding.reviewDownVote.alpha = 1f + binding.reviewUpVote.alpha = 0.6f + } + } + } + + private fun rateReview(rating: String) { + disableVote() + context.lifecycleScope.launch { + val result = Anilist.mutation.rateReview(review.id, rating) + if (result != null) { + withContext(Dispatchers.Main) { + val res = result.data.rateReview + review.rating = res.rating + review.ratingAmount = res.ratingAmount + review.userRating = res.userRating + userVote(review.userRating) + binding.reviewTotalVotes.text = review.rating.toString() + userVote(review.userRating) + enableVote() + } + } else { + withContext(Dispatchers.Main) { + toast( + context.getString(R.string.error_message, "response is null") + ) + enableVote() + } + } + } + } + + private fun disableVote() { + binding.reviewUpVote.setOnClickListener(null) + binding.reviewDownVote.setOnClickListener(null) + binding.reviewUpVote.isEnabled = false + binding.reviewDownVote.isEnabled = false + } + + private fun enableVote() { + binding.reviewUpVote.setOnClickListener { + if (review.userRating == "UP_VOTE") { + rateReview("NO_VOTE") + } else { + rateReview("UP_VOTE") + } + disableVote() + } + binding.reviewDownVote.setOnClickListener { + if (review.userRating == "DOWN_VOTE") { + rateReview("NO_VOTE") + } else { + rateReview("DOWN_VOTE") + } + disableVote() + } + binding.reviewUpVote.isEnabled = true + binding.reviewDownVote.isEnabled = true + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_review_view.xml b/app/src/main/res/layout/activity_review_view.xml index 6732d8f9..8ae33dc4 100644 --- a/app/src/main/res/layout/activity_review_view.xml +++ b/app/src/main/res/layout/activity_review_view.xml @@ -2,6 +2,7 @@ diff --git a/app/src/main/res/layout/item_developer.xml b/app/src/main/res/layout/item_developer.xml index c4c87d7b..d695edec 100644 --- a/app/src/main/res/layout/item_developer.xml +++ b/app/src/main/res/layout/item_developer.xml @@ -28,7 +28,7 @@ + android:text="@string/lorem_ipsum" + android:maxLines="2" + android:fontFamily="@font/poppins_semi_bold" /> diff --git a/app/src/main/res/layout/item_reviews.xml b/app/src/main/res/layout/item_reviews.xml new file mode 100644 index 00000000..0d2d5832 --- /dev/null +++ b/app/src/main/res/layout/item_reviews.xml @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From ab360b3a7594e624b25be3cc8a8273807c4c00de Mon Sep 17 00:00:00 2001 From: rebelonion <87634197+rebelonion@users.noreply.github.com> Date: Sun, 19 May 2024 14:17:58 -0500 Subject: [PATCH 5/6] feat: extension testing --- app/src/main/AndroidManifest.xml | 1 + .../ani/dantotsu/parsers/AniyomiAdapter.kt | 9 +- .../java/ani/dantotsu/parsers/BaseParser.kt | 45 ++- .../dantotsu/parsers/ExtensionSelectItem.kt | 38 ++ .../ani/dantotsu/parsers/ExtensionTestItem.kt | 367 ++++++++++++++++++ .../ExtensionTestSettingsBottomDialog.kt | 117 ++++++ .../dantotsu/parsers/ParserTestActivity.kt | 112 ++++++ .../dantotsu/settings/ExtensionsActivity.kt | 11 + .../settings/SettingsExtensionsActivity.kt | 17 + .../main/res/layout/activity_extensions.xml | 9 + .../main/res/layout/activity_parser_test.xml | 109 ++++++ .../bottom_sheet_extension_test_settings.xml | 100 +++++ .../main/res/layout/item_extension_select.xml | 40 ++ .../main/res/layout/item_extension_test.xml | 111 ++++++ app/src/main/res/values/strings.xml | 21 + 15 files changed, 1103 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/ani/dantotsu/parsers/ExtensionSelectItem.kt create mode 100644 app/src/main/java/ani/dantotsu/parsers/ExtensionTestItem.kt create mode 100644 app/src/main/java/ani/dantotsu/parsers/ExtensionTestSettingsBottomDialog.kt create mode 100644 app/src/main/java/ani/dantotsu/parsers/ParserTestActivity.kt create mode 100644 app/src/main/res/layout/activity_parser_test.xml create mode 100644 app/src/main/res/layout/bottom_sheet_extension_test_settings.xml create mode 100644 app/src/main/res/layout/item_extension_select.xml create mode 100644 app/src/main/res/layout/item_extension_test.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 68841a8d..738d7be6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -200,6 +200,7 @@ android:parentActivityName=".MainActivity" /> + diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index bacc5e22..df97b2ec 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt @@ -53,8 +53,11 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { override val name = extension.name override val saveName = extension.name - override val hostUrl = extension.sources.first().name + override val hostUrl = + (extension.sources.first() as? AnimeHttpSource)?.baseUrl ?: extension.sources.first().name override val isNSFW = extension.isNsfw + override val icon = extension.icon + override var selectDub: Boolean get() = getDub() set(value) { @@ -324,8 +327,10 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { override val name = extension.name override val saveName = extension.name - override val hostUrl = extension.sources.first().name + override val hostUrl = + (extension.sources.first() as? HttpSource)?.baseUrl ?: extension.sources.first().name override val isNSFW = extension.isNsfw + override val icon = extension.icon override suspend fun loadChapters( mangaLink: String, diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt index 2d83ab06..3ddfbb09 100644 --- a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt @@ -1,17 +1,21 @@ package ani.dantotsu.parsers +import android.graphics.drawable.Drawable import ani.dantotsu.FileUrl import ani.dantotsu.R import ani.dantotsu.currContext import ani.dantotsu.media.Media +import ani.dantotsu.okHttpClient import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.util.Logger import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.source.model.SManga import me.xdrop.fuzzywuzzy.FuzzySearch +import okhttp3.Request import java.io.Serializable import java.net.URLDecoder import java.net.URLEncoder +import kotlin.system.measureTimeMillis abstract class BaseParser { @@ -41,6 +45,11 @@ abstract class BaseParser { * **/ open val language = "English" + /** + * Icon of the site, can be null + */ + open val icon: Drawable? = null + /** * Search for Anime/Manga/Novel, returns a List of Responses * @@ -133,10 +142,39 @@ abstract class BaseParser { return response } + /** + * ping the site to check if it's working or not. + * @return Triple : First Int is the status code, Second Int is the response time in milliseconds, Third String is the response message. + */ + fun ping(): Triple { + val client = okHttpClient + var statusCode = 0 + var responseTime: Int? = null + var responseMessage = "" + try { + val request = Request.Builder() + .url(hostUrl) + .build() + responseTime = measureTimeMillis { + client.newCall(request).execute().use { response -> + statusCode = response.code + responseMessage = response.message + } + }.toInt() + } catch (e: Exception) { + Logger.log("Failed to ping $name") + statusCode = -1 + responseMessage = if (e.message.isNullOrEmpty()) "None" else e.message!! + Logger.log(e) + } + return Triple(statusCode, responseTime, responseMessage) + } /** * Used to get an existing Search Response which was selected by the user. - * **/ + * @param mediaId : The mediaId of the Media object. + * @return ShowResponse? : The ShowResponse object if found, else null. + */ open suspend fun loadSavedShowResponse(mediaId: Int): ShowResponse? { checkIfVariablesAreEmpty() return PrefManager.getNullableCustomVal( @@ -148,7 +186,10 @@ abstract class BaseParser { /** * Used to save Shows Response using `saveName`. - * **/ + * @param mediaId : The mediaId of the Media object. + * @param response : The ShowResponse object to save. + * @param selected : Boolean : If the ShowResponse was selected by the user or not. + */ open fun saveShowResponse(mediaId: Int, response: ShowResponse?, selected: Boolean = false) { if (response != null) { checkIfVariablesAreEmpty() diff --git a/app/src/main/java/ani/dantotsu/parsers/ExtensionSelectItem.kt b/app/src/main/java/ani/dantotsu/parsers/ExtensionSelectItem.kt new file mode 100644 index 00000000..8c9c20d5 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/ExtensionSelectItem.kt @@ -0,0 +1,38 @@ +package ani.dantotsu.parsers + +import android.graphics.drawable.Drawable +import android.view.View +import ani.dantotsu.R +import ani.dantotsu.databinding.ItemExtensionSelectBinding +import com.xwray.groupie.viewbinding.BindableItem + +class ExtensionSelectItem( + private val name: String, + private val image: Drawable?, + private var isSelected: Boolean, + val selectCallback: (String, Boolean) -> Unit +) : BindableItem() { + private lateinit var binding: ItemExtensionSelectBinding + + override fun bind(viewBinding: ItemExtensionSelectBinding, position: Int) { + binding = viewBinding + binding.extensionNameTextView.text = name + image?.let { + binding.extensionIconImageView.setImageDrawable(it) + } + binding.extensionCheckBox.setOnCheckedChangeListener(null) + binding.extensionCheckBox.isChecked = isSelected + binding.extensionCheckBox.setOnCheckedChangeListener { _, isChecked -> + isSelected = isChecked + selectCallback(name, isChecked) + } + } + + override fun getLayout(): Int { + return R.layout.item_extension_select + } + + override fun initializeViewBinding(view: View): ItemExtensionSelectBinding { + return ItemExtensionSelectBinding.bind(view) + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/ExtensionTestItem.kt b/app/src/main/java/ani/dantotsu/parsers/ExtensionTestItem.kt new file mode 100644 index 00000000..c87bf268 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/ExtensionTestItem.kt @@ -0,0 +1,367 @@ +package ani.dantotsu.parsers + +import android.content.Context +import android.view.View +import androidx.core.view.isVisible +import ani.dantotsu.R +import ani.dantotsu.databinding.ItemExtensionTestBinding +import ani.dantotsu.getThemeColor +import com.xwray.groupie.viewbinding.BindableItem +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ExtensionTestItem( + private var extensionType: String, + private var testType: String, + private var extension: BaseParser, + private var searchString: String = "Chainsaw Man" +) : BindableItem() { + private lateinit var binding: ItemExtensionTestBinding + private lateinit var context: Context + private var job: Job? = null + private var isRunning = false + private var pingResult: Triple? = null + private var searchResultSize: Int? = null + private var episodeResultSize: Int? = null + private var serverResultSize: Int? = null + + override fun bind(viewBinding: ItemExtensionTestBinding, position: Int) { + binding = viewBinding + context = binding.root.context + binding.extensionIconImageView.setImageDrawable(extension.icon) + binding.extensionNameTextView.text = extension.name + binding.extensionLoading.isVisible = isRunning + hideAllResults() + + pingResult() + searchResult() + episodeResult() + serverResult() + } + + override fun getLayout(): Int { + return R.layout.item_extension_test + } + + override fun initializeViewBinding(view: View): ItemExtensionTestBinding { + return ItemExtensionTestBinding.bind(view) + } + + private fun hideAllResults() { + if (::binding.isInitialized.not()) return + binding.searchResultText.isVisible = false + binding.episodeResultText.isVisible = false + binding.serverResultText.isVisible = false + } + + fun cancelJob() { + job?.cancel() + job = null + binding.extensionLoading.isVisible = false + } + + fun startTest() { + pingResult = null + searchResultSize = null + episodeResultSize = null + serverResultSize = null + isRunning = true + hideAllResults() + job?.cancel() + job = Job() + CoroutineScope(Dispatchers.IO + job!!).launch { + when (extensionType) { + "anime" -> { + val extension = extension as AnimeParser + runAnimeTest(extension) + } + + "manga" -> { + val extension = extension as MangaParser + runMangaTest(extension) + } + + "novel" -> { + val extension = extension as NovelParser + runNovelTest(extension) + } + } + } + } + + private suspend fun runAnimeTest(extension: AnimeParser) { + pingResult = extension.ping() + withContext(Dispatchers.Main) { + pingResult() + } + if (testType == "ping") { + done() + return + } + val searchResult = extension.search(searchString) + searchResultSize = searchResult.size + withContext(Dispatchers.Main) { + searchResult() + } + if (searchResultSize == 0 || testType == "basic") { + done() + return + } + val episodeResult = extension.loadEpisodes("", null, searchResult.first().sAnime!!) + episodeResultSize = episodeResult.size + withContext(Dispatchers.Main) { + episodeResult() + } + if (episodeResultSize == 0) { + done() + return + } + val serverResult = extension.loadVideoServers("", null, episodeResult.first().sEpisode!!) + serverResultSize = serverResult.size + withContext(Dispatchers.Main) { + serverResult() + } + + done() + } + + private suspend fun runMangaTest(extension: MangaParser) { + pingResult = extension.ping() + withContext(Dispatchers.Main) { + pingResult() + } + if (testType == "ping") { + done() + return + } + val searchResult = extension.search(searchString) + searchResultSize = searchResult.size + withContext(Dispatchers.Main) { + searchResult() + } + if (searchResultSize == 0 || testType == "basic") { + done() + return + } + val chapterResult = extension.loadChapters("", null, searchResult.first().sManga!!) + episodeResultSize = chapterResult.size + withContext(Dispatchers.Main) { + episodeResult() + } + if (episodeResultSize == 0) { + done() + return + } + val serverResult = extension.loadImages("", chapterResult.first().sChapter) + serverResultSize = serverResult.size + withContext(Dispatchers.Main) { + serverResult() + } + + withContext(Dispatchers.Main) { + if (::binding.isInitialized ) + binding.extensionLoading.isVisible = false + isRunning = false + } + } + + private suspend fun runNovelTest(extension: NovelParser) { + withContext(Dispatchers.Main) { + pingResult() + } + if (testType == "ping") { + done() + return + } + val searchResult = extension.search(searchString) + searchResultSize = searchResult.size + withContext(Dispatchers.Main) { + searchResult() + } + if (searchResultSize == 0 || testType == "basic") { + done() + return + } + val chapterResult = extension.loadBook(searchResult.first().link, null) + episodeResultSize = chapterResult.links.size + withContext(Dispatchers.Main) { + episodeResult() + serverResult() + } + + withContext(Dispatchers.Main) { + if (::binding.isInitialized ) + binding.extensionLoading.isVisible = false + isRunning = false + } + } + + private fun done() { + if (::binding.isInitialized.not()) return + binding.extensionLoading.isVisible = false + isRunning = false + } + + private fun pingResult() { + if (::binding.isInitialized.not()) return + if (extensionType == "novel") { + binding.pingResultText.isVisible = true + binding.pingResultText.text = context.getString(R.string.test_not_supported) + binding.pingResultText.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_round_info_24, 0, 0, 0 + ) + return + } + if (pingResult == null) { + binding.pingResultText.isVisible = false + return + } else { + binding.pingResultText.isVisible = true + } + binding.pingResultText.setTextColor( + context.getThemeColor(com.google.android.material.R.attr.colorPrimary) + ) + val (code, time, message) = pingResult!! + if (code == 200) { + binding.pingResultText.text = context.getString(R.string.ping_success, time.toString()) + binding.pingResultText.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_circle_check, 0, 0, 0 + ) + return + } + binding.pingResultText.text = + context.getString(R.string.ping_error, code.toString(), message) + binding.pingResultText.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_circle_cancel, 0, 0, 0 + ) + binding.pingResultText.setTextColor( + context.getThemeColor(com.google.android.material.R.attr.colorError) + ) + } + + private fun searchResult() { + if (::binding.isInitialized.not()) return + if (searchResultSize == null) { + binding.searchResultText.isVisible = false + return + } + binding.searchResultText.setTextColor( + context.getThemeColor(com.google.android.material.R.attr.colorPrimary) + ) + binding.searchResultText.isVisible = true + if (searchResultSize == 0) { + val text = context.getString(R.string.title_search_test, + context.getString(R.string.no_results_found)) + binding.searchResultText.text = text + binding.searchResultText.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_circle_cancel, 0, 0, 0 + ) + binding.searchResultText.setTextColor( + context.getThemeColor(com.google.android.material.R.attr.colorError) + ) + return + } + val text = context.getString(R.string.title_search_test, + context.getString(R.string.results_found, searchResultSize.toString())) + binding.searchResultText.text = text + binding.searchResultText.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_circle_check, 0, 0, 0 + ) + } + + private fun episodeResult() { + if (::binding.isInitialized.not()) return + if (episodeResultSize == null) { + binding.episodeResultText.isVisible = false + return + } + binding.episodeResultText.setTextColor( + context.getThemeColor(com.google.android.material.R.attr.colorPrimary) + ) + binding.episodeResultText.isVisible = true + if (episodeResultSize == 0) { + val text = when(extensionType) { + "anime" -> context.getString(R.string.episode_search_test, + context.getString(R.string.no_results_found)) + "manga" -> context.getString(R.string.chapter_search_test, + context.getString(R.string.no_results_found)) + else -> context.getString(R.string.book_search_test, + context.getString(R.string.no_results_found)) + } + binding.episodeResultText.text = text + binding.episodeResultText.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_circle_cancel, 0, 0, 0 + ) + binding.episodeResultText.setTextColor( + context.getThemeColor(com.google.android.material.R.attr.colorError) + ) + return + } + val text = when(extensionType) { + "anime" -> context.getString(R.string.episode_search_test, + context.getString(R.string.results_found, episodeResultSize.toString())) + "manga" -> context.getString(R.string.chapter_search_test, + context.getString(R.string.results_found, episodeResultSize.toString())) + else -> context.getString(R.string.book_search_test, + context.getString(R.string.results_found, episodeResultSize.toString())) + } + binding.episodeResultText.text = text + binding.episodeResultText.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_circle_check, 0, 0, 0 + ) + } + + private fun serverResult() { + if (::binding.isInitialized.not()) return + if (extensionType == "novel") { + binding.pingResultText.isVisible = true + binding.pingResultText.text = context.getString(R.string.test_not_supported) + binding.pingResultText.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_round_info_24, 0, 0, 0 + ) + return + } + if (serverResultSize == null) { + binding.serverResultText.isVisible = false + return + } + binding.serverResultText.setTextColor( + context.getThemeColor(com.google.android.material.R.attr.colorPrimary) + ) + binding.serverResultText.isVisible = true + if (serverResultSize == 0) { + val text = when(extensionType) { + "anime" -> context.getString(R.string.video_search_test, + context.getString(R.string.no_results_found)) + "manga" -> context.getString(R.string.image_search_test, + context.getString(R.string.no_results_found)) + else -> context.getString(R.string.book_search_test, + context.getString(R.string.no_results_found)) + } + binding.serverResultText.text = text + binding.serverResultText.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_circle_cancel, 0, 0, 0 + ) + binding.serverResultText.setTextColor( + context.getThemeColor(com.google.android.material.R.attr.colorError) + ) + return + } + val text = when(extensionType) { + "anime" -> context.getString(R.string.video_search_test, + context.getString(R.string.results_found, serverResultSize.toString())) + "manga" -> context.getString(R.string.image_search_test, + context.getString(R.string.results_found, serverResultSize.toString())) + else -> context.getString(R.string.book_search_test, + context.getString(R.string.results_found, serverResultSize.toString())) + } + binding.serverResultText.text = text + binding.serverResultText.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_circle_check, 0, 0, 0 + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/ExtensionTestSettingsBottomDialog.kt b/app/src/main/java/ani/dantotsu/parsers/ExtensionTestSettingsBottomDialog.kt new file mode 100644 index 00000000..fcedad3f --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/ExtensionTestSettingsBottomDialog.kt @@ -0,0 +1,117 @@ +package ani.dantotsu.parsers + +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import ani.dantotsu.BottomSheetDialogFragment +import ani.dantotsu.databinding.BottomSheetExtensionTestSettingsBinding +import ani.dantotsu.parsers.novel.NovelExtensionManager +import com.xwray.groupie.GroupieAdapter +import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager +import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ExtensionTestSettingsBottomDialog : BottomSheetDialogFragment() { + private var _binding: BottomSheetExtensionTestSettingsBinding? = null + private val binding get() = _binding!! + private val adapter: GroupieAdapter = GroupieAdapter() + private val animeExtension: AnimeExtensionManager = Injekt.get() + private val mangaExtensions: MangaExtensionManager = Injekt.get() + private val novelExtensions: NovelExtensionManager = Injekt.get() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = BottomSheetExtensionTestSettingsBinding.inflate(inflater, container, false) + return _binding?.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.extensionSelectionRecyclerView.adapter = adapter + binding.extensionSelectionRecyclerView.layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.VERTICAL, + false + ) + binding.animeRadioButton.setOnCheckedChangeListener { _, b -> + if (b) { + extensionType = "anime" + extensionsToTest.clear() + setupAdapter() + } + } + binding.mangaRadioButton.setOnCheckedChangeListener { _, b -> + if (b) { + extensionType = "manga" + extensionsToTest.clear() + setupAdapter() + } + } + binding.novelsRadioButton.setOnCheckedChangeListener { _, b -> + if (b) { + extensionType = "novel" + extensionsToTest.clear() + setupAdapter() + } + } + binding.pingRadioButton.setOnCheckedChangeListener { _, b -> + if (b) { + testType = "ping" + } + } + binding.basicRadioButton.setOnCheckedChangeListener { _, b -> + if (b) { + testType = "basic" + } + } + binding.fullRadioButton.setOnCheckedChangeListener { _, b -> + if (b) { + testType = "full" + } + } + setupAdapter() + } + + override fun onDestroyView() { + _binding = null + super.onDestroyView() + } + + private fun setupAdapter() { + val namesAndUrls: Map = when (extensionType) { + "anime" -> animeExtension.installedExtensionsFlow.value.associate { it.name to it.icon } + "manga" -> mangaExtensions.installedExtensionsFlow.value.associate { it.name to it.icon } + "novel" -> novelExtensions.installedExtensionsFlow.value.associate { it.name to it.icon } + else -> emptyMap() + } + adapter.clear() + namesAndUrls.forEach { (name, icon) -> + val isSelected = extensionsToTest.contains(name) + adapter.add(ExtensionSelectItem(name, icon, isSelected, ::selectedCallback)) + } + } + + private fun selectedCallback(name: String, isSelected: Boolean) { + if (isSelected) { + extensionsToTest.add(name) + } else { + extensionsToTest.remove(name) + } + } + + companion object { + fun newInstance(): ExtensionTestSettingsBottomDialog { + return ExtensionTestSettingsBottomDialog() + } + + var extensionType = "anime" + var testType = "ping" + var extensionsToTest: MutableList = mutableListOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/ParserTestActivity.kt b/app/src/main/java/ani/dantotsu/parsers/ParserTestActivity.kt new file mode 100644 index 00000000..7be91fe2 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/ParserTestActivity.kt @@ -0,0 +1,112 @@ +package ani.dantotsu.parsers + +import android.os.Bundle +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.LinearLayoutManager +import ani.dantotsu.R +import ani.dantotsu.databinding.ActivityParserTestBinding +import ani.dantotsu.initActivity +import ani.dantotsu.navBarHeight +import ani.dantotsu.statusBarHeight +import ani.dantotsu.themes.ThemeManager +import ani.dantotsu.toast +import com.xwray.groupie.GroupieAdapter + +class ParserTestActivity : AppCompatActivity() { + private lateinit var binding: ActivityParserTestBinding + val adapter = GroupieAdapter() + val extensionsToTest: MutableList = mutableListOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ThemeManager(this).applyTheme() + initActivity(this) + binding = ActivityParserTestBinding.inflate(layoutInflater) + binding.toolbar.updateLayoutParams { + topMargin = statusBarHeight + } + binding.extensionResultsRecyclerView.updateLayoutParams { + bottomMargin = navBarHeight + } + setContentView(binding.root) + + binding.extensionResultsRecyclerView.adapter = adapter + binding.extensionResultsRecyclerView.layoutManager = LinearLayoutManager( + this, + LinearLayoutManager.VERTICAL, + false + ) + binding.backButton.setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + binding.optionsLayout.setOnClickListener { + ExtensionTestSettingsBottomDialog.newInstance() + .show(supportFragmentManager, "extension_test_settings") + } + + binding.startButton.setOnClickListener { + if (ExtensionTestSettingsBottomDialog.extensionsToTest.isEmpty()) { + toast(R.string.no_extensions_selected) + return@setOnClickListener + } + extensionsToTest.forEach { + it.cancelJob() + } + extensionsToTest.clear() + adapter.clear() + when (ExtensionTestSettingsBottomDialog.extensionType) { + "anime" -> { + ExtensionTestSettingsBottomDialog.extensionsToTest.forEach { name -> + val extension = + AnimeSources.list.find { source -> source.name == name }?.get?.value + extension?.let { + extensionsToTest.add( + ExtensionTestItem( + "anime", + ExtensionTestSettingsBottomDialog.testType, + it + ) + ) + } + } + } + "manga" -> { + ExtensionTestSettingsBottomDialog.extensionsToTest.forEach { name -> + val extension = + MangaSources.list.find { source -> source.name == name }?.get?.value + extension?.let { + extensionsToTest.add( + ExtensionTestItem( + "manga", + ExtensionTestSettingsBottomDialog.testType, + it + ) + ) + } + } + } + "novel" -> { + ExtensionTestSettingsBottomDialog.extensionsToTest.forEach { name -> + val extension = + NovelSources.list.find { source -> source.name == name }?.get?.value + extension?.let { + extensionsToTest.add( + ExtensionTestItem( + "novel", + ExtensionTestSettingsBottomDialog.testType, + it + ) + ) + } + } + } + } + extensionsToTest.forEach { + adapter.add(it) + it.startTest() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt index 0125c3df..0fa65612 100644 --- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt @@ -1,6 +1,7 @@ package ani.dantotsu.settings import android.app.AlertDialog +import android.content.Intent import android.os.Bundle import android.text.Editable import android.text.TextWatcher @@ -13,6 +14,7 @@ import android.view.inputmethod.EditorInfo import android.widget.AutoCompleteTextView import android.widget.EditText import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter @@ -28,6 +30,7 @@ import ani.dantotsu.media.MediaType import ani.dantotsu.navBarHeight import ani.dantotsu.others.AndroidBug5497Workaround import ani.dantotsu.others.LanguageMapper +import ani.dantotsu.parsers.ParserTestActivity import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.statusBarHeight @@ -70,6 +73,14 @@ class ExtensionsActivity : AppCompatActivity() { bottomMargin = statusBarHeight + navBarHeight } + binding.testButton.setOnClickListener { + ContextCompat.startActivity( + this, + Intent(this, ParserTestActivity::class.java), + null + ) + } + val tabLayout = findViewById(R.id.tabLayout) val viewPager = findViewById(R.id.viewPager) viewPager.offscreenPageLimit = 1 diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt index 078d7b38..abf742ba 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt @@ -1,6 +1,7 @@ package ani.dantotsu.settings import android.app.AlertDialog +import android.content.Intent import android.os.Bundle import android.view.HapticFeedbackConstants import android.view.KeyEvent @@ -9,6 +10,7 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.EditText import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.LinearLayoutManager @@ -20,6 +22,7 @@ import ani.dantotsu.databinding.ItemRepositoryBinding import ani.dantotsu.initActivity import ani.dantotsu.media.MediaType import ani.dantotsu.navBarHeight +import ani.dantotsu.parsers.ParserTestActivity import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.statusBarHeight @@ -232,6 +235,20 @@ class SettingsExtensionsActivity : AppCompatActivity() { setExtensionOutput(it.attachView, MediaType.MANGA) } ), + Settings( + type = 1, + name = getString(R.string.extension_test), + desc = getString(R.string.extension_test_desc), + icon = R.drawable.ic_round_search_sources_24, + isActivity = true, + onClick = { + ContextCompat.startActivity( + context, + Intent(context, ParserTestActivity::class.java), + null + ) + } + ), Settings( type = 1, name = getString(R.string.user_agent), diff --git a/app/src/main/res/layout/activity_extensions.xml b/app/src/main/res/layout/activity_extensions.xml index 246c1785..a95edfed 100644 --- a/app/src/main/res/layout/activity_extensions.xml +++ b/app/src/main/res/layout/activity_extensions.xml @@ -41,6 +41,15 @@ android:layout_marginHorizontal="16dp" android:orientation="horizontal"> + + + + + + + + + + + + + + + + + + + + + +