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 {
applicationIdSuffix ".beta"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta"]
debuggable true
debuggable false
}
release {
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher"]

View file

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

View file

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

View file

@ -19,9 +19,16 @@ import kotlinx.coroutines.launch
suspend fun getUserId(context: Context, block: () -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
if (Discord.userid == null && Discord.token != null) {
if (!Discord.getUserData())
snackString(context.getString(R.string.error_loading_discord_user_data))
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
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 avatar: String? = null
private const val TOKEN = "discord_token"
const val TOKEN = "discord_token"
fun getSavedToken(context: Context): Boolean {
val sharedPref = context.getSharedPreferences(
@ -61,7 +61,7 @@ object Discord {
}
private var rpc : RPC? = null
suspend fun getUserData() = tryWithSuspend(true) {
/*suspend fun getUserData() = tryWithSuspend(true) {
if(rpc==null) {
val rpc = RPC(token!!, Dispatchers.IO).also { rpc = it }
val user: User = rpc.getUserData()
@ -70,7 +70,7 @@ object Discord {
rpc.close()
true
} else true
} ?: false
} ?: false*/
fun warning(context: Context) = CustomBottomDialog().apply {
@ -97,16 +97,20 @@ object Discord {
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 {
RPC(it, Dispatchers.IO).apply {
applicationId = "1163925779692912771"
applicationId = application_Id
smallImage = RPC.Link(
"Dantotsu",
"mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png"
small_Image
)
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.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.R
@ -67,11 +68,11 @@ class Login : AppCompatActivity() {
private fun login(token: String) {
if (token.isEmpty() || token == "null"){
snackString("Failed to retrieve token")
Toast.makeText(this, "Failed to retrieve token", Toast.LENGTH_SHORT).show()
finish()
return
}
snackString("Logged in successfully")
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
finish()
saveToken(this, token)
startMainActivity(this@Login)

View file

@ -1,6 +1,10 @@
package ani.dantotsu.connections.discord
import android.widget.Toast
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.Deferred
import kotlinx.coroutines.Job
@ -32,7 +36,12 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
allowStructuredMapKeys = 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()
.connectTimeout(10, SECONDS)
.readTimeout(10, SECONDS)
@ -56,16 +65,11 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
var startTimestamp: Long? = null
var stopTimestamp: Long? = null
enum class Type {
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
}
var buttons = mutableListOf<Link>()
data class Link(val label: String, val url: String)
private suspend fun createPresence(): String {
return json.encodeToString(Presence.Response(
val j = json.encodeToString(Presence.Response(
3,
Presence(
activities = listOf(
@ -95,16 +99,12 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
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() {
val response = Identity.Response(
@ -138,6 +138,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
}
}
if (!started) whenStarted = {
snackString("Discord message sent")
send.invoke()
whenStarted = null
}
@ -185,21 +186,21 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
}
override fun onMessage(webSocket: WebSocket, text: String) {
println("Discord Message : $text")
val map = json.decodeFromString<Res>(text)
seq = map.s
when (map.op) {
10 -> {
map.d as JsonObject
heartbeatInterval = map.d["heartbeat_interval"]!!.jsonPrimitive.long
sendHeartBeat()
sendIdentify()
snackString(map.t)
}
0 -> if (map.t == "READY") {
val user = json.decodeFromString<User.Response>(text).d.user
snackString(map.t)
started = true
whenStarted?.invoke(user)
}
@ -207,6 +208,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
1 -> {
if (scope.isActive) scope.cancel()
webSocket.send("{\"op\":1, \"d\":$seq}")
snackString(map.t)
}
11 -> sendHeartBeat()
@ -214,6 +216,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
9 -> {
sendHeartBeat()
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.json.*
@Serializable
data class User (
val verified: Boolean,
val verified: Boolean? = null,
val username: String,
@SerialName("purchased_flags")
val purchasedFlags: Long,
val purchasedFlags: Long? = null,
@SerialName("public_flags")
val publicFlags: Long,
val publicFlags: Long? = null,
val pronouns: String,
val pronouns: String? = null,
@SerialName("premium_type")
val premiumType: Long,
val premiumType: Long? = null,
val premium: Boolean,
val phone: String,
val premium: Boolean? = null,
val phone: String? = null,
@SerialName("nsfw_allowed")
val nsfwAllowed: Boolean,
val nsfwAllowed: Boolean? = null,
val mobile: Boolean,
val mobile: Boolean? = null,
@SerialName("mfa_enabled")
val mfaEnabled: Boolean,
val mfaEnabled: Boolean? = null,
val id: String,
@SerialName("global_name")
val globalName: String,
val globalName: String? = null,
val flags: Long,
val email: String,
val discriminator: String,
val desktop: Boolean,
val bio: String,
val flags: Long? = null,
val email: String? = null,
val discriminator: String? = null,
val desktop: Boolean? = null,
val bio: String? = null,
@SerialName("banner_color")
val bannerColor: String,
val bannerColor: String? = null,
val banner: JsonElement? = null,
@SerialName("avatar_decoration")
val avatarDecoration: JsonElement? = null,
val avatar: String,
val avatar: String? = null,
@SerialName("accent_color")
val accentColor: Long
val accentColor: Long? = null
) {
@Serializable
data class Response(

View file

@ -62,6 +62,9 @@ import ani.dantotsu.*
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
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.updateProgress
import ani.dantotsu.databinding.ActivityExoplayerBinding
@ -813,14 +816,14 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
fun fastForward() {
isFastForwarding = true
exoPlayer.setPlaybackSpeed(2f)
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2)
snackString("Playing at 2x speed")
}
fun stopFastForward() {
if (isFastForwarding) {
isFastForwarding = false
exoPlayer.setPlaybackSpeed(1f)
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed / 2)
snackString("Playing at normal speed")
}
}
@ -862,6 +865,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
override fun onLongClick(event: MotionEvent) {
if (settings.fastforward) fastForward()
}
override fun onDoubleClick(event: MotionEvent) {
doubleTap(true, event)
}
@ -994,22 +998,40 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
playbackPosition = loadData("${media.id}_${ep.number}", this) ?: 0
initPlayer()
preloading = false
rpc = Discord.defaultRPC()
rpc?.send {
type = RPC.Type.WATCHING
activityName = media.userPreferredName
val context = this
lifecycleScope.launch {
val presence = RPC.createPresence(RPC.Companion.RPCData(
applicationId = Discord.application_Id,
type = RPC.Type.WATCHING,
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)
}
media.shareLink?.let { link ->
buttons.add(0, RPC.Link(getString(R.string.view_anime), link))
)
)
)
val intent = Intent(context, DiscordService::class.java).apply {
putExtra("presence", presence)
}
DiscordServiceRunningSingleton.running = true
startService(intent)
}
updateProgress()
}
}
@ -1278,6 +1300,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
}
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
logger("url: ${video!!.file.url}")
logger("mimeType: $mimeType")
if (sub != null) {
val listofnotnullsubs = immutableListOf(sub).filterNotNull()
@ -1404,7 +1428,12 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
exoPlayer.release()
VideoCache.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) {
@ -1593,13 +1622,15 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
playerView.player?.trackSelectionParameters =
playerView.player?.trackSelectionParameters?.buildUpon()
?.setOverrideForType(
TrackSelectionOverride(it.mediaTrackGroup, it.length - 1))
TrackSelectionOverride(it.mediaTrackGroup, it.length - 1)
)
?.build()!!
} else if (it.type == 3) {
playerView.player?.trackSelectionParameters =
playerView.player?.trackSelectionParameters?.buildUpon()
?.addOverride(
TrackSelectionOverride(it.mediaTrackGroup, listOf()))
TrackSelectionOverride(it.mediaTrackGroup, listOf())
)
?.build()!!
}
}

View file

@ -3,6 +3,7 @@ package ani.dantotsu.media.manga.mangareader
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Bitmap
import android.os.Build
@ -25,12 +26,15 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
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.updateProgress
import ani.dantotsu.databinding.ActivityMangaReaderBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaSingleton
import ani.dantotsu.media.anime.ExoplayerView
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.manga.MangaNameAdapter
@ -121,7 +125,11 @@ class MangaReaderActivity : AppCompatActivity() {
override fun onDestroy() {
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()
}
@ -300,19 +308,36 @@ ThemeManager(this).applyTheme()
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
applySettings()
rpc?.close()
rpc = Discord.defaultRPC()
rpc?.send {
type = RPC.Type.WATCHING
activityName = media.userPreferredName
details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number)
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}"
media.cover?.let { cover ->
largeImage = RPC.Link(media.userPreferredName, cover)
}
media.shareLink?.let { link ->
buttons.add(0, RPC.Link(getString(R.string.view_manga), link))
val context = this
lifecycleScope.launch {
val presence = RPC.createPresence(RPC.Companion.RPCData(
applicationId = Discord.application_Id,
type = RPC.Type.WATCHING,
activityName = media.userPreferredName,
details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number),
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}",
largeImage = media.cover?.let { cover ->
RPC.Link(media.userPreferredName, cover)
},
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.avatar != null) {
binding.settingsDiscordAvatar.loadImage(Discord.avatar)
val id = getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE).getString("discord_id", null)
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.text = Discord.userid ?: Discord.token?.replace(Regex("."),"*")
binding.settingsDiscordUsername.text = username ?: Discord.token?.replace(Regex("."),"*")
binding.settingsDiscordLogin.setText(R.string.logout)
binding.settingsDiscordLogin.setOnClickListener {
Discord.removeSavedToken(this)

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff