diff --git a/app/src/main/java/ani/dantotsu/Functions.kt b/app/src/main/java/ani/dantotsu/Functions.kt index af525f99..15f873ea 100644 --- a/app/src/main/java/ani/dantotsu/Functions.kt +++ b/app/src/main/java/ani/dantotsu/Functions.kt @@ -1,5 +1,6 @@ package ani.dantotsu +import android.Manifest import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.app.Activity @@ -16,7 +17,6 @@ import android.content.res.Configuration import android.content.res.Resources.getSystem import android.graphics.Bitmap import android.graphics.Color -import android.Manifest import android.media.MediaScannerConnection import android.net.ConnectivityManager import android.net.NetworkCapabilities.* @@ -688,7 +688,11 @@ fun downloadsPermission(activity: AppCompatActivity): Boolean { }.toTypedArray() return if (requiredPermissions.isNotEmpty()) { - ActivityCompat.requestPermissions(activity, requiredPermissions, DOWNLOADS_PERMISSION_REQUEST_CODE) + ActivityCompat.requestPermissions( + activity, + requiredPermissions, + DOWNLOADS_PERMISSION_REQUEST_CODE + ) false } else { true @@ -1068,3 +1072,11 @@ suspend fun View.pop() { } delay(100) } + + +fun logToFile(context: Context, message: String) { + val externalFilesDir = context.getExternalFilesDir(null) + val file = File(externalFilesDir, "notifications.log") + file.appendText(message) + file.appendText("\n") +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt index 1e19ca8a..6bae267a 100644 --- a/app/src/main/java/ani/dantotsu/MainActivity.kt +++ b/app/src/main/java/ani/dantotsu/MainActivity.kt @@ -220,6 +220,21 @@ class MainActivity : AppCompatActivity() { } } + intent.extras?.let { extras -> + val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD") + val mediaId = extras.getInt("mediaId", -1) + val commentId = extras.getInt("commentId", -1) + + if (fragmentToLoad != null && mediaId != -1 && commentId != -1) { + val detailIntent = Intent(this, MediaDetailsActivity::class.java).apply { + putExtra("FRAGMENT_TO_LOAD", fragmentToLoad) + putExtra("mediaId", mediaId) + putExtra("commentId", commentId) + } + startActivity(detailIntent) + return + } + } val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode) if (!isOnline(this)) { snackString(this@MainActivity.getString(R.string.no_internet_connection)) diff --git a/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt b/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt index c807fe3b..63211580 100644 --- a/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt +++ b/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt @@ -17,9 +17,11 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json import okhttp3.FormBody +import okhttp3.OkHttpClient import okio.IOException import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.io.File object CommentsAPI { val address: String = "https://1224665.xyz:443" @@ -214,11 +216,13 @@ object CommentsAPI { return res } - suspend fun reportComment(commentId: Int, username: String, mediaTitle: String): Boolean { + suspend fun reportComment(commentId: Int, username: String, mediaTitle: String, reportedId: String): Boolean { val url = "$address/report/$commentId" val body = FormBody.Builder() .add("username", username) .add("mediaName", mediaTitle) + .add("reporter", Anilist.username ?: "unknown") + .add("reportedId", reportedId) .build() val request = requestBuilder() val json = try{ @@ -234,9 +238,9 @@ object CommentsAPI { return res } - suspend fun getNotifications(): NotificationResponse? { + suspend fun getNotifications(client: OkHttpClient): NotificationResponse? { val url = "$address/notification/reply" - val request = requestBuilder() + val request = requestBuilder(client) val json = try { request.get(url) } catch (e: IOException) { @@ -255,7 +259,7 @@ object CommentsAPI { return parsed } - suspend fun fetchAuthToken() { + suspend fun fetchAuthToken(client: OkHttpClient? = null) { if (authToken != null) return val MAX_RETRIES = 5 val tokenLifetime: Long = 1000 * 60 * 60 * 24 * 6 // 6 days @@ -276,7 +280,7 @@ object CommentsAPI { val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return repeat(MAX_RETRIES) { try { - val json = authRequest(token, url) + val json = authRequest(token, url, client) if (json.code == 200) { if (!json.text.startsWith("{")) throw IOException("Invalid response") val parsed = try { @@ -307,11 +311,11 @@ object CommentsAPI { snackString("Failed to login after multiple attempts") } - private suspend fun authRequest(token: String, url: String): NiceResponse { + private suspend fun authRequest(token: String, url: String, client: OkHttpClient? = null): NiceResponse { val body: FormBody = FormBody.Builder() .add("token", token) .build() - val request = requestBuilder() + val request = if (client != null) requestBuilder(client) else requestBuilder() return request.post(url, requestBody = body) } @@ -325,9 +329,9 @@ object CommentsAPI { return map } - private fun requestBuilder(): Requests { + private fun requestBuilder(client: OkHttpClient = Injekt.get().client): Requests { return Requests( - Injekt.get().client, + client, headerBuilder() ) } diff --git a/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt b/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt index 6842e924..25a02587 100644 --- a/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt +++ b/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt @@ -139,7 +139,7 @@ class CommentItem(val comment: Comment, dialogBuilder("Report Comment", "Only report comments that violate the rules. Are you sure you want to report this comment?") { val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) scope.launch { - val success = CommentsAPI.reportComment(comment.commentId, comment.username, commentsFragment.mediaName) + val success = CommentsAPI.reportComment(comment.commentId, comment.username, commentsFragment.mediaName, comment.userId) if (success) { snackString("Comment Reported") } diff --git a/app/src/main/java/ani/dantotsu/media/comments/CommentsFragment.kt b/app/src/main/java/ani/dantotsu/media/comments/CommentsFragment.kt index 3759ed78..5d2825c4 100644 --- a/app/src/main/java/ani/dantotsu/media/comments/CommentsFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/comments/CommentsFragment.kt @@ -348,7 +348,6 @@ class CommentsFragment : Fragment() { @SuppressLint("NotifyDataSetChanged") override fun onStart() { super.onStart() - adapter.notifyDataSetChanged() } @SuppressLint("NotifyDataSetChanged") @@ -423,6 +422,10 @@ class CommentsFragment : Fragment() { ) } } + + binding.commentsProgressBar.visibility = View.GONE + binding.commentsList.visibility = View.VISIBLE + adapter.add(section) } private fun sortComments(comments: List?): List { @@ -693,7 +696,7 @@ class CommentsFragment : Fragment() { .usePlugin(TaskListPlugin.create(activity)) .usePlugin(HtmlPlugin.create { plugin -> plugin.addHandler( - TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre") + TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a") ) }) .usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore { diff --git a/app/src/main/java/ani/dantotsu/notifications/MediaNameFetch.kt b/app/src/main/java/ani/dantotsu/notifications/MediaNameFetch.kt index a4795df9..47345104 100644 --- a/app/src/main/java/ani/dantotsu/notifications/MediaNameFetch.kt +++ b/app/src/main/java/ani/dantotsu/notifications/MediaNameFetch.kt @@ -13,6 +13,10 @@ class MediaNameFetch { mediaIds.forEachIndexed { index, mediaId -> query += """ media$index: Media(id: $mediaId) { + coverImage { + medium + color + } id title { romaji @@ -24,7 +28,7 @@ class MediaNameFetch { return query } - suspend fun fetchMediaTitles(ids: List): Map { + suspend fun fetchMediaTitles(ids: List): Map { return try { val url = "https://graphql.anilist.co/" val data = mapOf( @@ -40,15 +44,19 @@ class MediaNameFetch { data = data ) val mediaResponse = parseMediaResponseWithGson(response.text) - val mediaMap = mutableMapOf() + val mediaMap = mutableMapOf() mediaResponse.data.forEach { (_, mediaItem) -> - mediaMap[mediaItem.id] = mediaItem.title.romaji + mediaMap[mediaItem.id] = ReturnedData( + mediaItem.title.romaji, + mediaItem.coverImage.medium, + mediaItem.coverImage.color + ) } mediaMap } } catch (e: Exception) { - val errorMap = mutableMapOf() - ids.forEach { errorMap[it] = "Unknown" } + val errorMap = mutableMapOf() + ids.forEach { errorMap[it] = ReturnedData("Unknown", "", "#222222") } errorMap } } @@ -58,14 +66,17 @@ class MediaNameFetch { val type = object : TypeToken() {}.type return gson.fromJson(response, type) } + data class ReturnedData(val title: String, val coverImage: String, val color: String) data class MediaResponse(val data: Map) data class MediaItem( + val coverImage: MediaCoverImage, val id: Int, val title: MediaTitle ) data class MediaTitle(val romaji: String) + data class MediaCoverImage(val medium: String, val color: String) } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/notifications/NotificationWorker.kt b/app/src/main/java/ani/dantotsu/notifications/NotificationWorker.kt index 0a594485..054e6fdc 100644 --- a/app/src/main/java/ani/dantotsu/notifications/NotificationWorker.kt +++ b/app/src/main/java/ani/dantotsu/notifications/NotificationWorker.kt @@ -5,37 +5,55 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat import androidx.work.Worker import androidx.work.WorkerParameters +import ani.dantotsu.MainActivity import ani.dantotsu.R import ani.dantotsu.connections.comments.CommentsAPI import ani.dantotsu.media.MediaDetailsActivity +import ani.dantotsu.settings.saving.PrefManager import eu.kanade.tachiyomi.data.notification.Notifications import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import java.io.File +import java.text.DateFormat + class NotificationWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { override fun doWork(): Result { val scope = CoroutineScope(Dispatchers.IO) + //time in human readable format + val formattedTime = DateFormat.getDateTimeInstance().format(System.currentTimeMillis()) scope.launch { - val notifications = CommentsAPI.getNotifications() + PrefManager.init(applicationContext) //make sure prefs are initialized + val client = OkHttpClient() + CommentsAPI.fetchAuthToken(client) + val notifications = CommentsAPI.getNotifications(client) val mediaIds = notifications?.notifications?.map { it.mediaId } val names = MediaNameFetch.fetchMediaTitles(mediaIds ?: emptyList()) notifications?.notifications?.forEach { val title = "New Comment Reply" - val mediaName = names[it.mediaId] ?: "Unknown" + val mediaName = names[it.mediaId]?.title ?: "Unknown" val message = "${it.username} replied to your comment in $mediaName" val notification = createNotification( NotificationType.COMMENT_REPLY, message, title, it.mediaId, - it.commentId + it.commentId, + names[it.mediaId]?.color ?: "#222222", + names[it.mediaId]?.coverImage ?: "" ) if (ActivityCompat.checkSelfPermission( @@ -46,11 +64,10 @@ class NotificationWorker(appContext: Context, workerParams: WorkerParameters) : NotificationManagerCompat.from(applicationContext) .notify( NotificationType.COMMENT_REPLY.id, - Notifications.ID_COMMENT_REPLY, + it.commentId, notification ) } - } } return Result.success() @@ -61,11 +78,13 @@ class NotificationWorker(appContext: Context, workerParams: WorkerParameters) : message: String, title: String, mediaId: Int, - commentId: Int + commentId: Int, + color: String, + imageUrl: String ): android.app.Notification { val notification = when (notificationType) { NotificationType.COMMENT_REPLY -> { - val intent = Intent(applicationContext, MediaDetailsActivity::class.java).apply { + val intent = Intent(applicationContext, MainActivity::class.java).apply { putExtra("FRAGMENT_TO_LOAD", "COMMENTS") putExtra("mediaId", mediaId) putExtra("commentId", commentId) @@ -80,16 +99,46 @@ class NotificationWorker(appContext: Context, workerParams: WorkerParameters) : val builder = NotificationCompat.Builder(applicationContext, notificationType.id) .setContentTitle(title) .setContentText(message) - .setSmallIcon(R.drawable.ic_round_comment_24) + .setSmallIcon(R.drawable.notification_icon) .setPriority(NotificationCompat.PRIORITY_HIGH) .setContentIntent(pendingIntent) .setAutoCancel(true) + if (imageUrl.isNotEmpty()) { + val bitmap = getBitmapFromUrl(imageUrl) + if (bitmap != null) { + builder.setLargeIcon(bitmap) + } + } + if (color.isNotEmpty()) { + builder.color = Color.parseColor(color) + } builder.build() } } return notification } + private fun getBitmapFromVectorDrawable(context: Context, drawableId: Int): Bitmap? { + val drawable = ContextCompat.getDrawable(context, drawableId) ?: return null + val bitmap = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bitmap + } + + private fun getBitmapFromUrl(url: String): Bitmap? { + return try { + val inputStream = java.net.URL(url).openStream() + BitmapFactory.decodeStream(inputStream) + } catch (e: Exception) { + null + } + } + enum class NotificationType(val id: String) { COMMENT_REPLY(Notifications.CHANNEL_COMMENTS), } diff --git a/app/src/main/res/drawable/notification_icon.xml b/app/src/main/res/drawable/notification_icon.xml new file mode 100644 index 00000000..29e3dd6d --- /dev/null +++ b/app/src/main/res/drawable/notification_icon.xml @@ -0,0 +1,10 @@ + + + + + + +