This commit is contained in:
Finnley Somdahl 2023-11-26 02:36:27 -06:00
parent af326c8258
commit cf2d9ad654
16 changed files with 1131 additions and 544 deletions

View file

@ -29,7 +29,7 @@ android {
debug { debug {
applicationIdSuffix ".beta" applicationIdSuffix ".beta"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta"] manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta"]
debuggable true debuggable false
} }
release { release {
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher"] manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher"]

View file

@ -276,6 +276,10 @@
<service android:name=".download.manga.MangaDownloaderService" <service android:name=".download.manga.MangaDownloaderService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service android:name=".connections.discord.DiscordService"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application> </application>
</manifest> </manifest>

View file

@ -43,6 +43,7 @@ import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding import ani.dantotsu.databinding.ActivityMainBinding
import ani.dantotsu.databinding.ItemNavbarBinding import ani.dantotsu.databinding.ItemNavbarBinding
import ani.dantotsu.databinding.SplashScreenBinding import ani.dantotsu.databinding.SplashScreenBinding
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.OfflineMangaFragment import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.home.AnimeFragment import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment import ani.dantotsu.home.HomeFragment

View file

@ -19,9 +19,16 @@ import kotlinx.coroutines.launch
suspend fun getUserId(context: Context, block: () -> Unit) { suspend fun getUserId(context: Context, block: () -> Unit) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
if (Discord.userid == null && Discord.token != null) { val sharedPref = context.getSharedPreferences(
if (!Discord.getUserData()) context.getString(R.string.preference_file_key),
snackString(context.getString(R.string.error_loading_discord_user_data)) Context.MODE_PRIVATE
)
val token = sharedPref.getString("discord_token", null)
val userid = sharedPref.getString("discord_id", null)
if (userid == null && token != null) {
/*if (!Discord.getUserData())
snackString(context.getString(R.string.error_loading_discord_user_data))*/
//TODO: Discord.getUserData()
} }
} }

View file

@ -21,7 +21,7 @@ object Discord {
var userid: String? = null var userid: String? = null
var avatar: String? = null var avatar: String? = null
private const val TOKEN = "discord_token" const val TOKEN = "discord_token"
fun getSavedToken(context: Context): Boolean { fun getSavedToken(context: Context): Boolean {
val sharedPref = context.getSharedPreferences( val sharedPref = context.getSharedPreferences(
@ -61,7 +61,7 @@ object Discord {
} }
private var rpc : RPC? = null private var rpc : RPC? = null
suspend fun getUserData() = tryWithSuspend(true) { /*suspend fun getUserData() = tryWithSuspend(true) {
if(rpc==null) { if(rpc==null) {
val rpc = RPC(token!!, Dispatchers.IO).also { rpc = it } val rpc = RPC(token!!, Dispatchers.IO).also { rpc = it }
val user: User = rpc.getUserData() val user: User = rpc.getUserData()
@ -70,7 +70,7 @@ object Discord {
rpc.close() rpc.close()
true true
} else true } else true
} ?: false } ?: false*/
fun warning(context: Context) = CustomBottomDialog().apply { fun warning(context: Context) = CustomBottomDialog().apply {
@ -97,16 +97,20 @@ object Discord {
context.startActivity(intent) context.startActivity(intent)
} }
fun defaultRPC(): RPC? { const val application_Id = "1163925779692912771"
const val small_Image: String = "mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png"
/*fun defaultRPC(): RPC? {
return token?.let { return token?.let {
RPC(it, Dispatchers.IO).apply { RPC(it, Dispatchers.IO).apply {
applicationId = "1163925779692912771" applicationId = application_Id
smallImage = RPC.Link( smallImage = RPC.Link(
"Dantotsu", "Dantotsu",
"mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png" small_Image
) )
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/")) buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
} }
} }
} }*/
} }

View file

@ -0,0 +1,444 @@
package ani.dantotsu.connections.discord
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.IBinder
import android.os.PowerManager
import android.provider.MediaStore
import android.util.Log
import android.widget.Button
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.MainActivity
import ani.dantotsu.R
import ani.dantotsu.connections.discord.serializers.Activity
import ani.dantotsu.connections.discord.serializers.Presence
import ani.dantotsu.connections.discord.serializers.User
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.io.File
import java.io.OutputStreamWriter
import java.text.SimpleDateFormat
import java.util.Calendar
class DiscordService : Service() {
private var heartbeat : Int = 0
private var sequence : Int? = null
private var sessionId : String = ""
private var resume = false
private lateinit var logFile : File
private lateinit var webSocket: WebSocket
private lateinit var heartbeatThread : Thread
private lateinit var client : OkHttpClient
private lateinit var wakeLock: PowerManager.WakeLock
var presenceStore = ""
val json = Json {
encodeDefaults = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
coerceInputValues = true
}
var log = ""
override fun onCreate() {
super.onCreate()
log("Service onCreate()")
val powerManager = baseContext.getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "discordRPC:backgroundPresence")
wakeLock.acquire()
log("WakeLock Acquired")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
"discordPresence",
"Discord Presence Service Channel",
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
val intent = Intent(this, MainActivity::class.java).apply {
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val pendingIntent =
PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(this, "discordPresence")
.setSmallIcon(R.mipmap.ic_launcher_round)
.setContentTitle("Discord Presence")
.setContentText("Running in the background")
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_LOW)
startForeground(1, builder.build())
log("Foreground service started, notification shown")
client = OkHttpClient()
client.newWebSocket(
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
DiscordWebSocketListener()
)
client.dispatcher.executorService.shutdown()
SERVICE_RUNNING = true
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
log("Service onStartCommand()")
if(intent != null) {
if (intent.hasExtra("presence")) {
log("Service onStartCommand() setPresence")
var lPresence = intent.getStringExtra("presence")
if (this::webSocket.isInitialized) webSocket.send(lPresence!!)
presenceStore = lPresence!!
}else{
log("Service onStartCommand() no presence")
DiscordServiceRunningSingleton.running = false
stopSelf()
}
if (intent.hasExtra(ACTION_STOP_SERVICE)) {
log("Service onStartCommand() stopService")
stopSelf()
}
}
return START_REDELIVER_INTENT
}
override fun onDestroy() {
super.onDestroy()
log("Service Destroyed")
if (DiscordServiceRunningSingleton.running){
log("Accidental Service Destruction, restarting service")
val intent = Intent(baseContext, DiscordService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
baseContext.startForegroundService(intent)
} else {
baseContext.startService(intent)
}
} else {
setPresence(json.encodeToString(
Presence.Response(
3,
Presence(status = "offline")
)
))
wakeLock.release()
}
SERVICE_RUNNING = false
if(this::webSocket.isInitialized) webSocket.close(1000, "Closed by user")
//saveLogToFile()
}
fun saveProfile(response: String) {
val sharedPref = baseContext.getSharedPreferences(
baseContext.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
val user = json.decodeFromString<User.Response>(response).d.user
log("User data: $user")
with(sharedPref.edit()) {
putString("discord_username", user.username)
putString("discord_id", user.id)
putString("discord_avatar", user.avatar)
apply()
}
}
override fun onBind(p0: Intent?): IBinder? = null
inner class DiscordWebSocketListener : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
this@DiscordService.webSocket = webSocket
log("WebSocket: Opened")
}
override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
val json = JsonParser.parseString(text).asJsonObject
log("WebSocket: Received op code ${json.get("op")}")
when (json.get("op").asInt) {
0 -> {
if(json.has("s")) {
log("WebSocket: Sequence ${json.get("s")} Received")
sequence = json.get("s").asInt
}
if (json.get("t").asString != "READY") return
saveProfile(text)
log(text)
sessionId = json.get("d").asJsonObject.get("session_id").asString
log("WebSocket: SessionID ${json.get("d").asJsonObject.get("session_id")} Received")
if(presenceStore.isNotEmpty()) setPresence(presenceStore)
sendBroadcast(Intent("ServiceToConnectButton"))
}
1 -> {
log("WebSocket: Received Heartbeat request, sending heartbeat")
heartbeatThread.interrupt()
heartbeatSend(webSocket, sequence)
heartbeatThread = Thread(HeartbeatRunnable())
heartbeatThread.start()
}
7 -> {
resume = true
log("WebSocket: Requested to Restart, restarting")
webSocket.close(1000, "Requested to Restart by the server")
client = OkHttpClient()
client.newWebSocket(
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
DiscordWebSocketListener()
)
client.dispatcher.executorService.shutdown()
}
9 -> {
log("WebSocket: Invalid Session, restarting")
webSocket.close(1000, "Invalid Session")
Thread.sleep(5000)
client = OkHttpClient()
client.newWebSocket(
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
DiscordWebSocketListener()
)
client.dispatcher.executorService.shutdown()
}
10 -> {
heartbeat = json.get("d").asJsonObject.get("heartbeat_interval").asInt
heartbeatThread = Thread(HeartbeatRunnable())
heartbeatThread.start()
if(resume) {
log("WebSocket: Resuming because server requested")
resume()
resume = false
} else {
identify(webSocket, baseContext)
log("WebSocket: Identified")
}
}
11 -> {
log("WebSocket: Heartbeat ACKed")
heartbeatThread = Thread(HeartbeatRunnable())
heartbeatThread.start()
}
}
}
fun identify(webSocket: WebSocket, context: Context) {
val properties = JsonObject()
properties.addProperty("os","linux")
properties.addProperty("browser","unknown")
properties.addProperty("device","unknown")
val d = JsonObject()
d.addProperty("token", getToken(context))
d.addProperty("intents", 0)
d.add("properties", properties)
val payload = JsonObject()
payload.addProperty("op",2)
payload.add("d", d)
webSocket.send(payload.toString())
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
super.onFailure(webSocket, t, response)
t.message?.let { Log.d("WebSocket", "onFailure() $it") }
log("WebSocket: Error, onFailure() reason: ${t.message}")
client = OkHttpClient()
client.newWebSocket(
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
DiscordWebSocketListener()
)
client.dispatcher.executorService.shutdown()
if(!heartbeatThread.isInterrupted) { heartbeatThread.interrupt() }
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
Log.d("WebSocket", "onClosing() $code $reason")
if(!heartbeatThread.isInterrupted) { heartbeatThread.interrupt() }
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason)
Log.d("WebSocket", "onClosed() $code $reason")
if(code >= 4000) {
log("WebSocket: Error, code: $code reason: $reason")
client = OkHttpClient()
client.newWebSocket(
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
DiscordWebSocketListener()
)
client.dispatcher.executorService.shutdown()
return
}
}
}
fun getToken(context: Context): String {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
val token = sharedPref.getString(Discord.TOKEN, null)
if(token == null) {
log("WebSocket: Token not found")
errorNotification("Could not set the presence", "token not found")
return ""
}
else{
return token
}
}
fun heartbeatSend( webSocket: WebSocket, seq: Int? ) {
val json = JsonObject()
json.addProperty("op",1)
json.addProperty("d", seq)
webSocket.send(json.toString())
}
private fun errorNotification(title : String, text: String) {
val intent = Intent(this@DiscordService, MainActivity::class.java).apply {
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val pendingIntent =
PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(this@DiscordService, "discordPresence")
.setSmallIcon(R.mipmap.ic_launcher_round)
.setContentTitle(title)
.setContentText(text)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
val notificationManager = NotificationManagerCompat.from(applicationContext)
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
//TODO: Request permission
return
}
notificationManager.notify(2, builder.build())
log("Error Notified")
}
fun saveSimpleTestPresence() {
val file = File(baseContext.cacheDir, "payload")
//fill with test payload
val payload = JsonObject()
payload.addProperty("op", 3)
payload.add("d", JsonObject().apply {
addProperty("status", "online")
addProperty("afk", false)
add("activities", JsonArray().apply {
add(JsonObject().apply {
addProperty("name", "Test")
addProperty("type", 0)
})
})
})
file.writeText(payload.toString())
log("WebSocket: Simple Test Presence Saved")
}
fun setPresence(String: String) {
log("WebSocket: Sending Presence payload")
log(String)
webSocket.send(String)
}
fun log(string: String) {
Log.d("WebSocket_Discord", string)
//log += "${SimpleDateFormat("HH:mm:ss").format(Calendar.getInstance().time)} $string\n"
}
fun saveLogToFile() {
val fileName = "log_${System.currentTimeMillis()}.txt"
// ContentValues to store file metadata
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
}
}
// Inserting the file in the MediaStore
val resolver = baseContext.contentResolver
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
} else {
val directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(directory, fileName)
// Make sure the Downloads directory exists
if (!directory.exists()) {
directory.mkdirs()
}
// Use FileProvider to get the URI for the file
val authority = "${baseContext.packageName}.provider" // Adjust with your app's package name
Uri.fromFile(file)
}
// Writing to the file
uri?.let {
resolver.openOutputStream(it).use { outputStream ->
OutputStreamWriter(outputStream).use { writer ->
writer.write(log)
}
}
} ?: run {
log("Error saving log file")
}
}
fun resume() {
log("Sending Resume payload")
val d = JsonObject()
d.addProperty("token", getToken(baseContext))
d.addProperty("session_id", sessionId)
d.addProperty("seq", sequence)
val json = JsonObject()
json.addProperty("op", 6)
json.add("d", d)
log(json.toString())
webSocket.send(json.toString())
}
inner class HeartbeatRunnable : Runnable {
override fun run() {
try {
Thread.sleep(heartbeat.toLong())
heartbeatSend(webSocket, sequence)
log("WebSocket: Heartbeat Sent")
} catch (e:InterruptedException) {}
}
}
companion object {
var SERVICE_RUNNING = false
const val ACTION_STOP_SERVICE = "ACTION_STOP_SERVICE"
}
}
object DiscordServiceRunningSingleton {
var running = false
}

View file

@ -7,6 +7,7 @@ import android.os.Bundle
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.R import ani.dantotsu.R
@ -67,11 +68,11 @@ class Login : AppCompatActivity() {
private fun login(token: String) { private fun login(token: String) {
if (token.isEmpty() || token == "null"){ if (token.isEmpty() || token == "null"){
snackString("Failed to retrieve token") Toast.makeText(this, "Failed to retrieve token", Toast.LENGTH_SHORT).show()
finish() finish()
return return
} }
snackString("Logged in successfully") Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
finish() finish()
saveToken(this, token) saveToken(this, token)
startMainActivity(this@Login) startMainActivity(this@Login)

View file

@ -1,6 +1,10 @@
package ani.dantotsu.connections.discord package ani.dantotsu.connections.discord
import android.widget.Toast
import ani.dantotsu.connections.discord.serializers.* import ani.dantotsu.connections.discord.serializers.*
import ani.dantotsu.currContext
import ani.dantotsu.logger
import ani.dantotsu.snackString
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -32,7 +36,12 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
allowStructuredMapKeys = true allowStructuredMapKeys = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
} }
enum class Type {
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
}
data class Link(val label: String, val url: String)
/*
private val client = OkHttpClient.Builder() private val client = OkHttpClient.Builder()
.connectTimeout(10, SECONDS) .connectTimeout(10, SECONDS)
.readTimeout(10, SECONDS) .readTimeout(10, SECONDS)
@ -56,16 +65,11 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
var startTimestamp: Long? = null var startTimestamp: Long? = null
var stopTimestamp: Long? = null var stopTimestamp: Long? = null
enum class Type {
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
}
var buttons = mutableListOf<Link>() var buttons = mutableListOf<Link>()
data class Link(val label: String, val url: String)
private suspend fun createPresence(): String { private suspend fun createPresence(): String {
return json.encodeToString(Presence.Response( val j = json.encodeToString(Presence.Response(
3, 3,
Presence( Presence(
activities = listOf( activities = listOf(
@ -95,16 +99,12 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
status = status status = status
) )
)) ))
logger("Presence: $j")
return j
} }
@Serializable
data class KizzyApi(val id: String)
val api = "https://kizzy-api.vercel.app/image?url="
private suspend fun String.discordUrl(): String? {
if (startsWith("mp:")) return this
val json = app.get("$api$this").parsedSafe<KizzyApi>()
return json?.id
}
private fun sendIdentify() { private fun sendIdentify() {
val response = Identity.Response( val response = Identity.Response(
@ -138,6 +138,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
} }
} }
if (!started) whenStarted = { if (!started) whenStarted = {
snackString("Discord message sent")
send.invoke() send.invoke()
whenStarted = null whenStarted = null
} }
@ -185,21 +186,21 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
} }
override fun onMessage(webSocket: WebSocket, text: String) { override fun onMessage(webSocket: WebSocket, text: String) {
println("Discord Message : $text")
val map = json.decodeFromString<Res>(text) val map = json.decodeFromString<Res>(text)
seq = map.s seq = map.s
when (map.op) { when (map.op) {
10 -> { 10 -> {
map.d as JsonObject map.d as JsonObject
heartbeatInterval = map.d["heartbeat_interval"]!!.jsonPrimitive.long heartbeatInterval = map.d["heartbeat_interval"]!!.jsonPrimitive.long
sendHeartBeat() sendHeartBeat()
sendIdentify() sendIdentify()
snackString(map.t)
} }
0 -> if (map.t == "READY") { 0 -> if (map.t == "READY") {
val user = json.decodeFromString<User.Response>(text).d.user val user = json.decodeFromString<User.Response>(text).d.user
snackString(map.t)
started = true started = true
whenStarted?.invoke(user) whenStarted?.invoke(user)
} }
@ -207,6 +208,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
1 -> { 1 -> {
if (scope.isActive) scope.cancel() if (scope.isActive) scope.cancel()
webSocket.send("{\"op\":1, \"d\":$seq}") webSocket.send("{\"op\":1, \"d\":$seq}")
snackString(map.t)
} }
11 -> sendHeartBeat() 11 -> sendHeartBeat()
@ -214,6 +216,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
9 -> { 9 -> {
sendHeartBeat() sendHeartBeat()
sendIdentify() sendIdentify()
snackString(map.t)
} }
} }
} }
@ -232,6 +235,68 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
} }
} }
} }
*/
companion object {
data class RPCData(
val applicationId: String? = null,
val type: Type? = null,
val activityName: String? = null,
val details: String? = null,
val state: String? = null,
val largeImage: Link? = null,
val smallImage: Link? = null,
val status: String? = null,
val startTimestamp: Long? = null,
val stopTimestamp: Long? = null,
val buttons: MutableList<Link> = mutableListOf()
)
@Serializable
data class KizzyApi(val id: String)
val api = "https://kizzy-api.vercel.app/image?url="
private suspend fun String.discordUrl(): String? {
if (startsWith("mp:")) return this
val json = app.get("$api$this").parsedSafe<KizzyApi>()
return json?.id
}
suspend fun createPresence(data: RPCData): String {
val json = Json {
encodeDefaults = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
}
return json.encodeToString(Presence.Response(
3,
Presence(
activities = listOf(
Activity(
name = data.activityName,
state = data.state,
details = data.details,
type = data.type?.ordinal,
timestamps = if (data.startTimestamp != null)
Activity.Timestamps(data.startTimestamp, data.stopTimestamp)
else null,
assets = Activity.Assets(
largeImage = data.largeImage?.url?.discordUrl(),
largeText = data.largeImage?.label,
smallImage = data.smallImage?.url?.discordUrl(),
smallText = data.smallImage?.label
),
buttons = data.buttons.map { it.label },
metadata = Activity.Metadata(
buttonUrls = data.buttons.map { it.url }
),
applicationId = data.applicationId,
)
),
afk = true,
since = data.startTimestamp,
status = data.status
)
))
}
}
} }

View file

@ -5,56 +5,57 @@ import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.* import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
@Serializable @Serializable
data class User ( data class User (
val verified: Boolean, val verified: Boolean? = null,
val username: String, val username: String,
@SerialName("purchased_flags") @SerialName("purchased_flags")
val purchasedFlags: Long, val purchasedFlags: Long? = null,
@SerialName("public_flags") @SerialName("public_flags")
val publicFlags: Long, val publicFlags: Long? = null,
val pronouns: String, val pronouns: String? = null,
@SerialName("premium_type") @SerialName("premium_type")
val premiumType: Long, val premiumType: Long? = null,
val premium: Boolean, val premium: Boolean? = null,
val phone: String, val phone: String? = null,
@SerialName("nsfw_allowed") @SerialName("nsfw_allowed")
val nsfwAllowed: Boolean, val nsfwAllowed: Boolean? = null,
val mobile: Boolean, val mobile: Boolean? = null,
@SerialName("mfa_enabled") @SerialName("mfa_enabled")
val mfaEnabled: Boolean, val mfaEnabled: Boolean? = null,
val id: String, val id: String,
@SerialName("global_name") @SerialName("global_name")
val globalName: String, val globalName: String? = null,
val flags: Long, val flags: Long? = null,
val email: String, val email: String? = null,
val discriminator: String, val discriminator: String? = null,
val desktop: Boolean, val desktop: Boolean? = null,
val bio: String, val bio: String? = null,
@SerialName("banner_color") @SerialName("banner_color")
val bannerColor: String, val bannerColor: String? = null,
val banner: JsonElement? = null, val banner: JsonElement? = null,
@SerialName("avatar_decoration") @SerialName("avatar_decoration")
val avatarDecoration: JsonElement? = null, val avatarDecoration: JsonElement? = null,
val avatar: String, val avatar: String? = null,
@SerialName("accent_color") @SerialName("accent_color")
val accentColor: Long val accentColor: Long? = null
) { ) {
@Serializable @Serializable
data class Response( data class Response(

View file

@ -62,6 +62,9 @@ import ani.dantotsu.*
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.discord.Discord import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.discord.DiscordService
import ani.dantotsu.connections.discord.DiscordService.Companion.ACTION_STOP_SERVICE
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
import ani.dantotsu.connections.discord.RPC import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityExoplayerBinding import ani.dantotsu.databinding.ActivityExoplayerBinding
@ -813,14 +816,14 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
fun fastForward() { fun fastForward() {
isFastForwarding = true isFastForwarding = true
exoPlayer.setPlaybackSpeed(2f) exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2)
snackString("Playing at 2x speed") snackString("Playing at 2x speed")
} }
fun stopFastForward() { fun stopFastForward() {
if (isFastForwarding) { if (isFastForwarding) {
isFastForwarding = false isFastForwarding = false
exoPlayer.setPlaybackSpeed(1f) exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed / 2)
snackString("Playing at normal speed") snackString("Playing at normal speed")
} }
} }
@ -862,6 +865,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
override fun onLongClick(event: MotionEvent) { override fun onLongClick(event: MotionEvent) {
if (settings.fastforward) fastForward() if (settings.fastforward) fastForward()
} }
override fun onDoubleClick(event: MotionEvent) { override fun onDoubleClick(event: MotionEvent) {
doubleTap(true, event) doubleTap(true, event)
} }
@ -994,22 +998,40 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
playbackPosition = loadData("${media.id}_${ep.number}", this) ?: 0 playbackPosition = loadData("${media.id}_${ep.number}", this) ?: 0
initPlayer() initPlayer()
preloading = false preloading = false
rpc = Discord.defaultRPC() val context = this
rpc?.send {
type = RPC.Type.WATCHING lifecycleScope.launch {
activityName = media.userPreferredName val presence = RPC.createPresence(RPC.Companion.RPCData(
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString( applicationId = Discord.application_Id,
R.string.episode_num, type = RPC.Type.WATCHING,
ep.number activityName = media.userPreferredName,
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
R.string.episode_num,
ep.number
),
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
largeImage = media.cover?.let { RPC.Link(media.userPreferredName, it) },
smallImage = RPC.Link(
"Dantotsu",
Discord.small_Image
),
buttons = mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link(
"Stream on Dantotsu",
"https://github.com/rebelonion/Dantotsu/"
)
)
) )
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}" )
media.cover?.let { cover ->
largeImage = RPC.Link(media.userPreferredName, cover) val intent = Intent(context, DiscordService::class.java).apply {
} putExtra("presence", presence)
media.shareLink?.let { link ->
buttons.add(0, RPC.Link(getString(R.string.view_anime), link))
} }
DiscordServiceRunningSingleton.running = true
startService(intent)
} }
updateProgress() updateProgress()
} }
} }
@ -1278,6 +1300,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
} }
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType) val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
logger("url: ${video!!.file.url}")
logger("mimeType: $mimeType")
if (sub != null) { if (sub != null) {
val listofnotnullsubs = immutableListOf(sub).filterNotNull() val listofnotnullsubs = immutableListOf(sub).filterNotNull()
@ -1310,7 +1334,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
) )
.setMaxVideoSize(1, 1) .setMaxVideoSize(1, 1)
//.setOverrideForType( //.setOverrideForType(
// TrackSelectionOverride(trackSelector, 2)) // TrackSelectionOverride(trackSelector, 2))
) )
if (playbackPosition != 0L && !changingServer && !settings.alwaysContinue) { if (playbackPosition != 0L && !changingServer && !settings.alwaysContinue) {
@ -1329,17 +1353,17 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
) )
AlertDialog.Builder(this, R.style.DialogTheme) AlertDialog.Builder(this, R.style.DialogTheme)
.setTitle(getString(R.string.continue_from, time)).apply { .setTitle(getString(R.string.continue_from, time)).apply {
setCancelable(false) setCancelable(false)
setPositiveButton(getString(R.string.yes)) { d, _ -> setPositiveButton(getString(R.string.yes)) { d, _ ->
buildExoplayer() buildExoplayer()
d.dismiss() d.dismiss()
} }
setNegativeButton(getString(R.string.no)) { d, _ -> setNegativeButton(getString(R.string.no)) { d, _ ->
playbackPosition = 0L playbackPosition = 0L
buildExoplayer() buildExoplayer()
d.dismiss() d.dismiss()
} }
}.show() }.show()
} else buildExoplayer() } else buildExoplayer()
} }
@ -1404,7 +1428,12 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
exoPlayer.release() exoPlayer.release()
VideoCache.release() VideoCache.release()
mediaSession?.release() mediaSession?.release()
rpc?.close() val stopIntent = Intent(this, DiscordService::class.java).apply {
putExtra(ACTION_STOP_SERVICE, true)
}
DiscordServiceRunningSingleton.running = false
startService(stopIntent)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -1589,17 +1618,19 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
println("Track__: ${it.isSelected}") println("Track__: ${it.isSelected}")
println("Track__: ${it.type}") println("Track__: ${it.type}")
println("Track__: ${it.mediaTrackGroup.id}") println("Track__: ${it.mediaTrackGroup.id}")
if (it.type == 3 && it.mediaTrackGroup.id == "1:"){ if (it.type == 3 && it.mediaTrackGroup.id == "1:") {
playerView.player?.trackSelectionParameters = playerView.player?.trackSelectionParameters =
playerView.player?.trackSelectionParameters?.buildUpon() playerView.player?.trackSelectionParameters?.buildUpon()
?.setOverrideForType( ?.setOverrideForType(
TrackSelectionOverride(it.mediaTrackGroup, it.length - 1)) TrackSelectionOverride(it.mediaTrackGroup, it.length - 1)
)
?.build()!! ?.build()!!
}else if(it.type == 3){ } else if (it.type == 3) {
playerView.player?.trackSelectionParameters = playerView.player?.trackSelectionParameters =
playerView.player?.trackSelectionParameters?.buildUpon() playerView.player?.trackSelectionParameters?.buildUpon()
?.addOverride( ?.addOverride(
TrackSelectionOverride(it.mediaTrackGroup, listOf())) TrackSelectionOverride(it.mediaTrackGroup, listOf())
)
?.build()!! ?.build()!!
} }
} }

View file

@ -3,6 +3,7 @@ package ani.dantotsu.media.manga.mangareader
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
@ -25,12 +26,15 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.discord.Discord import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.discord.DiscordService
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
import ani.dantotsu.connections.discord.RPC import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityMangaReaderBinding import ani.dantotsu.databinding.ActivityMangaReaderBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaSingleton import ani.dantotsu.media.MediaSingleton
import ani.dantotsu.media.anime.ExoplayerView
import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.manga.MangaNameAdapter import ani.dantotsu.media.manga.MangaNameAdapter
@ -121,7 +125,11 @@ class MangaReaderActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
mangaCache.clear() mangaCache.clear()
rpc?.close() val stopIntent = Intent(this, DiscordService::class.java).apply {
putExtra(DiscordService.ACTION_STOP_SERVICE, true)
}
DiscordServiceRunningSingleton.running = false
startService(stopIntent)
super.onDestroy() super.onDestroy()
} }
@ -300,19 +308,36 @@ ThemeManager(this).applyTheme()
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: "" binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: "" binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
applySettings() applySettings()
rpc?.close() val context = this
rpc = Discord.defaultRPC() lifecycleScope.launch {
rpc?.send { val presence = RPC.createPresence(RPC.Companion.RPCData(
type = RPC.Type.WATCHING applicationId = Discord.application_Id,
activityName = media.userPreferredName type = RPC.Type.WATCHING,
details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number) activityName = media.userPreferredName,
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}" details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number),
media.cover?.let { cover -> state = "${chap.number}/${media.manga?.totalChapters ?: "??"}",
largeImage = RPC.Link(media.userPreferredName, cover) largeImage = media.cover?.let { cover ->
} RPC.Link(media.userPreferredName, cover)
media.shareLink?.let { link -> },
buttons.add(0, RPC.Link(getString(R.string.view_manga), link)) smallImage = RPC.Link(
"Dantotsu",
Discord.small_Image
),
buttons = mutableListOf(
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
RPC.Link(
"Stream on Dantotsu",
"https://github.com/rebelonion/Dantotsu/"
)
)
)
)
val intent = Intent(context, DiscordService::class.java).apply {
putExtra("presence", presence)
} }
DiscordServiceRunningSingleton.running = true
startService(intent)
} }
} }
} }

View file

@ -578,11 +578,14 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
} }
if (Discord.token != null) { if (Discord.token != null) {
if (Discord.avatar != null) { val id = getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE).getString("discord_id", null)
binding.settingsDiscordAvatar.loadImage(Discord.avatar) val avatar = getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE).getString("discord_avatar", null)
val username = getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE).getString("discord_username", null)
if (id != null && avatar != null) {
binding.settingsDiscordAvatar.loadImage("https://cdn.discordapp.com/avatars/$id/$avatar.png")
} }
binding.settingsDiscordUsername.visibility = View.VISIBLE binding.settingsDiscordUsername.visibility = View.VISIBLE
binding.settingsDiscordUsername.text = Discord.userid ?: Discord.token?.replace(Regex("."),"*") binding.settingsDiscordUsername.text = username ?: Discord.token?.replace(Regex("."),"*")
binding.settingsDiscordLogin.setText(R.string.logout) binding.settingsDiscordLogin.setText(R.string.logout)
binding.settingsDiscordLogin.setOnClickListener { binding.settingsDiscordLogin.setOnClickListener {
Discord.removeSavedToken(this) Discord.removeSavedToken(this)

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.anime
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import ani.dantotsu.snackString
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
@ -116,7 +117,7 @@ class AnimeExtensionManager(
api.findExtensions() api.findExtensions()
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
withUIContext { context.toast("Failed to get extensions list") } withUIContext { snackString("Failed to get extensions list") }
emptyList() emptyList()
} }

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.manga
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import ani.dantotsu.snackString
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
@ -116,7 +117,7 @@ class MangaExtensionManager(
api.findExtensions() api.findExtensions()
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
withUIContext { context.toast("Failed to get manga extensions") } withUIContext { snackString("Failed to get manga extensions") }
emptyList() emptyList()
} }

View file

@ -97,9 +97,7 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:numColumns="auto_fit" android:numColumns="auto_fit"
android:paddingLeft="8dp" android:columnWidth="128dp"
android:paddingRight="8dp"
android:columnWidth="108dp"
android:verticalSpacing="10dp" android:verticalSpacing="10dp"
android:horizontalSpacing="10dp" android:horizontalSpacing="10dp"
android:padding="10dp" android:padding="10dp"

File diff suppressed because it is too large Load diff