feat: better notification

This commit is contained in:
rebelonion 2024-02-28 01:27:48 -06:00
parent 2f7c6e734e
commit e256fb1560
8 changed files with 131 additions and 27 deletions

View file

@ -1,5 +1,6 @@
package ani.dantotsu package ani.dantotsu
import android.Manifest
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
@ -16,7 +17,6 @@ import android.content.res.Configuration
import android.content.res.Resources.getSystem import android.content.res.Resources.getSystem
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.Manifest
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities.* import android.net.NetworkCapabilities.*
@ -688,7 +688,11 @@ fun downloadsPermission(activity: AppCompatActivity): Boolean {
}.toTypedArray() }.toTypedArray()
return if (requiredPermissions.isNotEmpty()) { return if (requiredPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(activity, requiredPermissions, DOWNLOADS_PERMISSION_REQUEST_CODE) ActivityCompat.requestPermissions(
activity,
requiredPermissions,
DOWNLOADS_PERMISSION_REQUEST_CODE
)
false false
} else { } else {
true true
@ -1068,3 +1072,11 @@ suspend fun View.pop() {
} }
delay(100) 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")
}

View file

@ -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) val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
if (!isOnline(this)) { if (!isOnline(this)) {
snackString(this@MainActivity.getString(R.string.no_internet_connection)) snackString(this@MainActivity.getString(R.string.no_internet_connection))

View file

@ -17,9 +17,11 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient
import okio.IOException import okio.IOException
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
object CommentsAPI { object CommentsAPI {
val address: String = "https://1224665.xyz:443" val address: String = "https://1224665.xyz:443"
@ -214,11 +216,13 @@ object CommentsAPI {
return res 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 url = "$address/report/$commentId"
val body = FormBody.Builder() val body = FormBody.Builder()
.add("username", username) .add("username", username)
.add("mediaName", mediaTitle) .add("mediaName", mediaTitle)
.add("reporter", Anilist.username ?: "unknown")
.add("reportedId", reportedId)
.build() .build()
val request = requestBuilder() val request = requestBuilder()
val json = try{ val json = try{
@ -234,9 +238,9 @@ object CommentsAPI {
return res return res
} }
suspend fun getNotifications(): NotificationResponse? { suspend fun getNotifications(client: OkHttpClient): NotificationResponse? {
val url = "$address/notification/reply" val url = "$address/notification/reply"
val request = requestBuilder() val request = requestBuilder(client)
val json = try { val json = try {
request.get(url) request.get(url)
} catch (e: IOException) { } catch (e: IOException) {
@ -255,7 +259,7 @@ object CommentsAPI {
return parsed return parsed
} }
suspend fun fetchAuthToken() { suspend fun fetchAuthToken(client: OkHttpClient? = null) {
if (authToken != null) return if (authToken != null) return
val MAX_RETRIES = 5 val MAX_RETRIES = 5
val tokenLifetime: Long = 1000 * 60 * 60 * 24 * 6 // 6 days 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 val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return
repeat(MAX_RETRIES) { repeat(MAX_RETRIES) {
try { try {
val json = authRequest(token, url) val json = authRequest(token, url, client)
if (json.code == 200) { if (json.code == 200) {
if (!json.text.startsWith("{")) throw IOException("Invalid response") if (!json.text.startsWith("{")) throw IOException("Invalid response")
val parsed = try { val parsed = try {
@ -307,11 +311,11 @@ object CommentsAPI {
snackString("Failed to login after multiple attempts") 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() val body: FormBody = FormBody.Builder()
.add("token", token) .add("token", token)
.build() .build()
val request = requestBuilder() val request = if (client != null) requestBuilder(client) else requestBuilder()
return request.post(url, requestBody = body) return request.post(url, requestBody = body)
} }
@ -325,9 +329,9 @@ object CommentsAPI {
return map return map
} }
private fun requestBuilder(): Requests { private fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
return Requests( return Requests(
Injekt.get<NetworkHelper>().client, client,
headerBuilder() headerBuilder()
) )
} }

View file

@ -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?") { 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()) val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch { 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) { if (success) {
snackString("Comment Reported") snackString("Comment Reported")
} }

View file

@ -348,7 +348,6 @@ class CommentsFragment : Fragment() {
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
adapter.notifyDataSetChanged()
} }
@SuppressLint("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<Comment>?): List<Comment> { private fun sortComments(comments: List<Comment>?): List<Comment> {
@ -693,7 +696,7 @@ class CommentsFragment : Fragment() {
.usePlugin(TaskListPlugin.create(activity)) .usePlugin(TaskListPlugin.create(activity))
.usePlugin(HtmlPlugin.create { plugin -> .usePlugin(HtmlPlugin.create { plugin ->
plugin.addHandler( 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 { .usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {

View file

@ -13,6 +13,10 @@ class MediaNameFetch {
mediaIds.forEachIndexed { index, mediaId -> mediaIds.forEachIndexed { index, mediaId ->
query += """ query += """
media$index: Media(id: $mediaId) { media$index: Media(id: $mediaId) {
coverImage {
medium
color
}
id id
title { title {
romaji romaji
@ -24,7 +28,7 @@ class MediaNameFetch {
return query return query
} }
suspend fun fetchMediaTitles(ids: List<Int>): Map<Int, String> { suspend fun fetchMediaTitles(ids: List<Int>): Map<Int, ReturnedData> {
return try { return try {
val url = "https://graphql.anilist.co/" val url = "https://graphql.anilist.co/"
val data = mapOf( val data = mapOf(
@ -40,15 +44,19 @@ class MediaNameFetch {
data = data data = data
) )
val mediaResponse = parseMediaResponseWithGson(response.text) val mediaResponse = parseMediaResponseWithGson(response.text)
val mediaMap = mutableMapOf<Int, String>() val mediaMap = mutableMapOf<Int, ReturnedData>()
mediaResponse.data.forEach { (_, mediaItem) -> mediaResponse.data.forEach { (_, mediaItem) ->
mediaMap[mediaItem.id] = mediaItem.title.romaji mediaMap[mediaItem.id] = ReturnedData(
mediaItem.title.romaji,
mediaItem.coverImage.medium,
mediaItem.coverImage.color
)
} }
mediaMap mediaMap
} }
} catch (e: Exception) { } catch (e: Exception) {
val errorMap = mutableMapOf<Int, String>() val errorMap = mutableMapOf<Int, ReturnedData>()
ids.forEach { errorMap[it] = "Unknown" } ids.forEach { errorMap[it] = ReturnedData("Unknown", "", "#222222") }
errorMap errorMap
} }
} }
@ -58,14 +66,17 @@ class MediaNameFetch {
val type = object : TypeToken<MediaResponse>() {}.type val type = object : TypeToken<MediaResponse>() {}.type
return gson.fromJson(response, type) return gson.fromJson(response, type)
} }
data class ReturnedData(val title: String, val coverImage: String, val color: String)
data class MediaResponse(val data: Map<String, MediaItem>) data class MediaResponse(val data: Map<String, MediaItem>)
data class MediaItem( data class MediaItem(
val coverImage: MediaCoverImage,
val id: Int, val id: Int,
val title: MediaTitle val title: MediaTitle
) )
data class MediaTitle(val romaji: String) data class MediaTitle(val romaji: String)
data class MediaCoverImage(val medium: String, val color: String)
} }
} }

View file

@ -5,37 +5,55 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager 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.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import ani.dantotsu.MainActivity
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.comments.CommentsAPI import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.settings.saving.PrefManager
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import java.io.File
import java.text.DateFormat
class NotificationWorker(appContext: Context, workerParams: WorkerParameters) : class NotificationWorker(appContext: Context, workerParams: WorkerParameters) :
Worker(appContext, workerParams) { Worker(appContext, workerParams) {
override fun doWork(): Result { override fun doWork(): Result {
val scope = CoroutineScope(Dispatchers.IO) val scope = CoroutineScope(Dispatchers.IO)
//time in human readable format
val formattedTime = DateFormat.getDateTimeInstance().format(System.currentTimeMillis())
scope.launch { 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 mediaIds = notifications?.notifications?.map { it.mediaId }
val names = MediaNameFetch.fetchMediaTitles(mediaIds ?: emptyList()) val names = MediaNameFetch.fetchMediaTitles(mediaIds ?: emptyList())
notifications?.notifications?.forEach { notifications?.notifications?.forEach {
val title = "New Comment Reply" 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 message = "${it.username} replied to your comment in $mediaName"
val notification = createNotification( val notification = createNotification(
NotificationType.COMMENT_REPLY, NotificationType.COMMENT_REPLY,
message, message,
title, title,
it.mediaId, it.mediaId,
it.commentId it.commentId,
names[it.mediaId]?.color ?: "#222222",
names[it.mediaId]?.coverImage ?: ""
) )
if (ActivityCompat.checkSelfPermission( if (ActivityCompat.checkSelfPermission(
@ -46,11 +64,10 @@ class NotificationWorker(appContext: Context, workerParams: WorkerParameters) :
NotificationManagerCompat.from(applicationContext) NotificationManagerCompat.from(applicationContext)
.notify( .notify(
NotificationType.COMMENT_REPLY.id, NotificationType.COMMENT_REPLY.id,
Notifications.ID_COMMENT_REPLY, it.commentId,
notification notification
) )
} }
} }
} }
return Result.success() return Result.success()
@ -61,11 +78,13 @@ class NotificationWorker(appContext: Context, workerParams: WorkerParameters) :
message: String, message: String,
title: String, title: String,
mediaId: Int, mediaId: Int,
commentId: Int commentId: Int,
color: String,
imageUrl: String
): android.app.Notification { ): android.app.Notification {
val notification = when (notificationType) { val notification = when (notificationType) {
NotificationType.COMMENT_REPLY -> { 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("FRAGMENT_TO_LOAD", "COMMENTS")
putExtra("mediaId", mediaId) putExtra("mediaId", mediaId)
putExtra("commentId", commentId) putExtra("commentId", commentId)
@ -80,16 +99,46 @@ class NotificationWorker(appContext: Context, workerParams: WorkerParameters) :
val builder = NotificationCompat.Builder(applicationContext, notificationType.id) val builder = NotificationCompat.Builder(applicationContext, notificationType.id)
.setContentTitle(title) .setContentTitle(title)
.setContentText(message) .setContentText(message)
.setSmallIcon(R.drawable.ic_round_comment_24) .setSmallIcon(R.drawable.notification_icon)
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setAutoCancel(true) .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() builder.build()
} }
} }
return notification 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) { enum class NotificationType(val id: String) {
COMMENT_REPLY(Notifications.CHANNEL_COMMENTS), COMMENT_REPLY(Notifications.CHANNEL_COMMENTS),
} }

View file

@ -0,0 +1,10 @@
<vector android:height="200dp" android:viewportHeight="768"
android:viewportWidth="768" android:width="200dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M-118.57,-113.45l1005.14,0l0,994.92l-1005.14,0z"/>
<path android:fillColor="#FFFFFF"
android:pathData="m-277.06,-109.04c0,87.59 70.57,158.7 158.49,161.06L-118.57,-109.02L886.57,-109.02L886.57,877L1131.17,877L1131.17,-109.04ZM886.57,877L-118.57,877l0,0.04l1005.14,0zM-118.57,877L-118.57,716c-87.89,2.36 -158.45,73.43 -158.49,161zM-118.57,716c1.49,-0.04 2.95,-0.22 4.45,-0.22L384,715.78C569.12,715.78 719.18,567.23 719.18,384.01 719.18,200.77 569.1,52.24 384,52.24L-114.12,52.24c-1.5,0 -2.96,-0.18 -4.45,-0.22l0,143.59L384,195.61c105.11,0 190.34,84.36 190.34,188.4 0,104.06 -85.23,188.4 -190.34,188.4L-118.57,572.41ZM-118.57,572.41L-118.57,195.61L-363.17,195.61l0,376.8z" android:strokeWidth="0"/>
<path android:fillColor="#FFFFFF"
android:pathData="m496.85,350.69 l-147.92,-84.53c-25.92,-14.81 -58.3,3.7 -58.3,33.32l0,169.06c0,29.62 32.4,48.13 58.3,33.32L496.85,417.33c25.92,-14.81 25.92,-51.83 0,-66.64z" android:strokeWidth="0"/>
</group>
</vector>