rpc fix
This commit is contained in:
parent
af326c8258
commit
cf2d9ad654
16 changed files with 1131 additions and 544 deletions
|
@ -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"]
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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/"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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()!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue