Initial commit
This commit is contained in:
commit
21bfbfb139
520 changed files with 47819 additions and 0 deletions
1
app/src/main/java/ani/dantotsu/others/.gitignore
vendored
Normal file
1
app/src/main/java/ani/dantotsu/others/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
DisabledReports.kt
|
57
app/src/main/java/ani/dantotsu/others/AniSkip.kt
Normal file
57
app/src/main/java/ani/dantotsu/others/AniSkip.kt
Normal file
|
@ -0,0 +1,57 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.net.URLEncoder
|
||||
|
||||
object AniSkip {
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun getResult(malId: Int, episodeNumber: Int, episodeLength: Long, useProxyForTimeStamps: Boolean): List<Stamp>? {
|
||||
val url =
|
||||
"https://api.aniskip.com/v2/skip-times/$malId/$episodeNumber?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=$episodeLength"
|
||||
return tryWithSuspend {
|
||||
val a = if(useProxyForTimeStamps)
|
||||
client.get("https://corsproxy.io/?${URLEncoder.encode(url, "utf-8").replace("+", "%20")}")
|
||||
else
|
||||
client.get(url)
|
||||
val res = a.parsed<AniSkipResponse>()
|
||||
if (res.found) res.results else null
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AniSkipResponse(
|
||||
val found: Boolean,
|
||||
val results: List<Stamp>?,
|
||||
val message: String?,
|
||||
val statusCode: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Stamp(
|
||||
val interval: AniSkipInterval,
|
||||
val skipType: String,
|
||||
val skipId: String,
|
||||
val episodeLength: Double
|
||||
)
|
||||
|
||||
|
||||
fun String.getType(): String {
|
||||
return when (this) {
|
||||
"op" -> "Opening"
|
||||
"ed" -> "Ending"
|
||||
"recap" -> "Recap"
|
||||
"mixed-ed" -> "Mixed Ending"
|
||||
"mixed-op" -> "Mixed Opening"
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AniSkipInterval(
|
||||
val startTime: Double,
|
||||
val endTime: Double
|
||||
)
|
||||
}
|
223
app/src/main/java/ani/dantotsu/others/AppUpdater.kt
Normal file
223
app/src/main/java/ani/dantotsu/others/AppUpdater.kt
Normal file
|
@ -0,0 +1,223 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.*
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
object AppUpdater {
|
||||
suspend fun check(activity: FragmentActivity, post:Boolean=false) {
|
||||
if(post) snackString(currContext()?.getString(R.string.checking_for_update))
|
||||
val repo = activity.getString(R.string.repo)
|
||||
tryWithSuspend {
|
||||
val (md, version) = if(BuildConfig.DEBUG){
|
||||
val res = client.get("https://api.github.com/repos/$repo/releases")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
val r = res.filter { it.prerelease }.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val v = r.tagName.substringAfter("v","")
|
||||
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||
}else{
|
||||
val res =
|
||||
client.get("https://raw.githubusercontent.com/$repo/main/stable.md").text
|
||||
res to res.substringAfter("# ").substringBefore("\n")
|
||||
}
|
||||
|
||||
logger("Git Version : $version")
|
||||
val dontShow = loadData("dont_ask_for_update_$version") ?: false
|
||||
if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread {
|
||||
CustomBottomDialog.newInstance().apply {
|
||||
setTitleText("${if (BuildConfig.DEBUG) "Beta " else ""}Update " + currContext()!!.getString(R.string.available))
|
||||
addView(
|
||||
TextView(activity).apply {
|
||||
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||
markWon.setMarkdown(this, md)
|
||||
}
|
||||
)
|
||||
|
||||
setCheck(currContext()!!.getString(R.string.dont_show_again, version), false) { isChecked ->
|
||||
if (isChecked) {
|
||||
saveData("dont_ask_for_update_$version", true)
|
||||
}
|
||||
}
|
||||
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
|
||||
MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<GithubResponse>().assets?.find {
|
||||
it.browserDownloadURL.endsWith("apk")
|
||||
}?.browserDownloadURL.apply {
|
||||
if (this != null) activity.downloadUpdate(version, this)
|
||||
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
setNegativeButton(currContext()!!.getString(R.string.cope)) {
|
||||
dismiss()
|
||||
}
|
||||
show(activity.supportFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
else{
|
||||
if(post) snackString(currContext()?.getString(R.string.no_update_found))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun compareVersion(version: String): Boolean {
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
return BuildConfig.VERSION_NAME != version
|
||||
else {
|
||||
fun toDouble(list: List<String>): Double {
|
||||
return list.mapIndexed { i: Int, s: String ->
|
||||
when (i) {
|
||||
0 -> s.toDouble() * 100
|
||||
1 -> s.toDouble() * 10
|
||||
2 -> s.toDouble()
|
||||
else -> s.toDoubleOrNull()?: 0.0
|
||||
}
|
||||
}.sum()
|
||||
}
|
||||
|
||||
val new = toDouble(version.split("."))
|
||||
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
|
||||
return new > curr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Blatantly kanged from https://github.com/LagradOst/CloudStream-3/blob/master/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt
|
||||
private fun Activity.downloadUpdate(version: String, url: String): Boolean {
|
||||
|
||||
toast(getString(R.string.downloading_update, version))
|
||||
|
||||
val downloadManager = this.getSystemService<DownloadManager>()!!
|
||||
|
||||
val request = DownloadManager.Request(Uri.parse(url))
|
||||
.setMimeType("application/vnd.android.package-archive")
|
||||
.setTitle("Downloading Dantotsu $version")
|
||||
.setDestinationInExternalPublicDir(
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
"Dantotsu $version.apk"
|
||||
)
|
||||
.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
|
||||
.setAllowedOverRoaming(true)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
|
||||
val id = try {
|
||||
downloadManager.enqueue(request)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
-1
|
||||
}
|
||||
if (id == -1L) return true
|
||||
registerReceiver(
|
||||
object : BroadcastReceiver() {
|
||||
@SuppressLint("Range")
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
try {
|
||||
val downloadId = intent?.getLongExtra(
|
||||
DownloadManager.EXTRA_DOWNLOAD_ID, id
|
||||
) ?: id
|
||||
|
||||
val query = DownloadManager.Query()
|
||||
query.setFilterById(downloadId)
|
||||
val c = downloadManager.query(query)
|
||||
|
||||
if (c.moveToFirst()) {
|
||||
val columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||
if (DownloadManager.STATUS_SUCCESSFUL == c
|
||||
.getInt(columnIndex)
|
||||
) {
|
||||
c.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI)
|
||||
val uri = Uri.parse(
|
||||
c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||
)
|
||||
openApk(this@downloadUpdate, uri)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
fun openApk(context: Context, uri: Uri) {
|
||||
try {
|
||||
uri.path?.let {
|
||||
val contentUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
BuildConfig.APPLICATION_ID + ".provider",
|
||||
File(it)
|
||||
)
|
||||
val installIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
|
||||
data = contentUri
|
||||
}
|
||||
context.startActivity(installIntent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
|
||||
|
||||
@Serializable
|
||||
data class GithubResponse(
|
||||
@SerialName("html_url")
|
||||
val htmlUrl: String,
|
||||
@SerialName("tag_name")
|
||||
val tagName: String,
|
||||
val prerelease: Boolean,
|
||||
@SerialName("created_at")
|
||||
val createdAt : String,
|
||||
val body: String? = null,
|
||||
val assets: List<Asset>? = null
|
||||
) {
|
||||
@Serializable
|
||||
data class Asset(
|
||||
@SerialName("browser_download_url")
|
||||
val browserDownloadURL: String
|
||||
)
|
||||
|
||||
fun timeStamp(): Long {
|
||||
return dateFormat.parse(createdAt)!!.time
|
||||
}
|
||||
}
|
||||
}
|
93
app/src/main/java/ani/dantotsu/others/CustomBottomDialog.kt
Normal file
93
app/src/main/java/ani/dantotsu/others/CustomBottomDialog.kt
Normal file
|
@ -0,0 +1,93 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.databinding.BottomSheetCustomBinding
|
||||
|
||||
open class CustomBottomDialog : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetCustomBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val viewList = mutableListOf<View>()
|
||||
fun addView(view: View) {
|
||||
viewList.add(view)
|
||||
}
|
||||
var title: String?=null
|
||||
fun setTitleText(string: String){
|
||||
title = string
|
||||
}
|
||||
|
||||
private var checkText: String? = null
|
||||
private var checkChecked: Boolean = false
|
||||
private var checkCallback: ((Boolean) -> Unit)? = null
|
||||
|
||||
fun setCheck(text: String, checked: Boolean, callback: ((Boolean) -> Unit)) {
|
||||
checkText = text
|
||||
checkChecked = checked
|
||||
checkCallback = callback
|
||||
}
|
||||
|
||||
private var negativeText: String? = null
|
||||
private var negativeCallback: (() -> Unit)? = null
|
||||
fun setNegativeButton(text: String, callback: (() -> Unit)) {
|
||||
negativeText = text
|
||||
negativeCallback = callback
|
||||
}
|
||||
|
||||
private var positiveText: String? = null
|
||||
private var positiveCallback: (() -> Unit)? = null
|
||||
fun setPositiveButton(text: String, callback: (() -> Unit)) {
|
||||
positiveText = text
|
||||
positiveCallback = callback
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = BottomSheetCustomBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.bottomSheerCustomTitle.text = title
|
||||
viewList.forEach {
|
||||
binding.bottomDialogCustomContainer.addView(it)
|
||||
}
|
||||
if (checkText != null) binding.bottomDialogCustomCheckBox.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = checkText
|
||||
isChecked = checkChecked
|
||||
setOnCheckedChangeListener { _, checked ->
|
||||
checkCallback?.invoke(checked)
|
||||
}
|
||||
}
|
||||
|
||||
if(negativeText!=null) binding.bottomDialogCustomNegative.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = negativeText
|
||||
setOnClickListener {
|
||||
negativeCallback?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
if(positiveText!=null) binding.bottomDialogCustomPositive.apply {
|
||||
visibility = View.VISIBLE
|
||||
text = positiveText
|
||||
setOnClickListener {
|
||||
positiveCallback?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = CustomBottomDialog()
|
||||
}
|
||||
|
||||
}
|
186
app/src/main/java/ani/dantotsu/others/Download.kt
Normal file
186
app/src/main/java/ani/dantotsu/others/Download.kt
Normal file
|
@ -0,0 +1,186 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.parsers.Book
|
||||
import ani.dantotsu.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
object Download {
|
||||
@Suppress("DEPRECATION")
|
||||
private fun isPackageInstalled(packageName: String, packageManager: PackageManager): Boolean {
|
||||
return try {
|
||||
packageManager.getPackageInfo(packageName, 0)
|
||||
true
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDownloadDir(context: Context): File {
|
||||
val direct: File
|
||||
if (loadData<Boolean>("sd_dl") == true) {
|
||||
val arrayOfFiles = ContextCompat.getExternalFilesDirs(context, null)
|
||||
val parentDirectory = arrayOfFiles[1].toString()
|
||||
direct = File(parentDirectory)
|
||||
if (!direct.exists()) direct.mkdirs()
|
||||
} else {
|
||||
direct = File("storage/emulated/0/${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/")
|
||||
if (!direct.exists()) direct.mkdirs()
|
||||
}
|
||||
return direct
|
||||
}
|
||||
|
||||
fun download(context: Context, episode: Episode, animeTitle: String) {
|
||||
toast(context.getString(R.string.downloading))
|
||||
val extractor = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return
|
||||
val video =
|
||||
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else return
|
||||
val regex = "[\\\\/:*?\"<>|]".toRegex()
|
||||
val aTitle = animeTitle.replace(regex, "")
|
||||
val title = "Episode ${episode.number}${if (episode.title != null) " - ${episode.title}" else ""}".replace(regex, "")
|
||||
|
||||
val notif = "$title : $aTitle"
|
||||
val folder = "/Anime/${aTitle}/"
|
||||
val fileName = "$title${if (video.size != null) "(${video.size}p)" else ""}.mp4"
|
||||
val file = video.file
|
||||
download(context, file, fileName, folder, notif)
|
||||
}
|
||||
|
||||
fun download(context: Context, book:Book, pos:Int, novelTitle:String){
|
||||
toast(currContext()?.getString(R.string.downloading))
|
||||
val regex = "[\\\\/:*?\"<>|]".toRegex()
|
||||
val nTitle = novelTitle.replace(regex, "")
|
||||
val title = book.name.replace(regex, "")
|
||||
|
||||
val notif = "$title : $nTitle"
|
||||
val folder = "/Novel/${nTitle}/"
|
||||
val fileName = "$title.epub"
|
||||
val file = book.links[pos]
|
||||
download(context, file, fileName, folder, notif)
|
||||
}
|
||||
|
||||
fun download(context: Context, file: FileUrl, fileName: String, folder: String, notif: String? = null) {
|
||||
if(!file.url.startsWith("http"))
|
||||
toast(context.getString(R.string.invalid_url))
|
||||
else
|
||||
when (loadData<Int>("settings_download_manager", context, false) ?: 0) {
|
||||
1 -> oneDM(context, file, notif ?: fileName)
|
||||
2 -> adm(context, file, fileName, folder)
|
||||
else -> defaultDownload(context, file, fileName, folder, notif ?: fileName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun defaultDownload(context: Context, file: FileUrl, fileName: String, folder: String, notif: String) {
|
||||
val manager = context.getSystemService(AppCompatActivity.DOWNLOAD_SERVICE) as DownloadManager
|
||||
val request: DownloadManager.Request = DownloadManager.Request(Uri.parse(file.url))
|
||||
file.headers.forEach {
|
||||
request.addRequestHeader(it.key, it.value)
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
|
||||
val arrayOfFiles = ContextCompat.getExternalFilesDirs(context, null)
|
||||
if (loadData<Boolean>("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
|
||||
val parentDirectory = arrayOfFiles[1].toString() + folder
|
||||
val direct = File(parentDirectory)
|
||||
if (!direct.exists()) direct.mkdirs()
|
||||
request.setDestinationUri(Uri.fromFile(File("$parentDirectory$fileName")))
|
||||
} else {
|
||||
val direct = File(Environment.DIRECTORY_DOWNLOADS + "/Dantotsu$folder")
|
||||
if (!direct.exists()) direct.mkdirs()
|
||||
request.setDestinationInExternalPublicDir(
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
"/Dantotsu$folder$fileName"
|
||||
)
|
||||
}
|
||||
request.setTitle(notif)
|
||||
manager.enqueue(request)
|
||||
toast(currContext()?.getString(R.string.started_downloading, notif))
|
||||
} catch (e: SecurityException) {
|
||||
toast(currContext()?.getString(R.string.permission_required))
|
||||
} catch (e: Exception) {
|
||||
toast(e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun oneDM(context: Context, file: FileUrl, notif: String) {
|
||||
val appName = if (isPackageInstalled("idm.internet.download.manager.plus", context.packageManager)) {
|
||||
"idm.internet.download.manager.plus"
|
||||
} else if (isPackageInstalled("idm.internet.download.manager", context.packageManager)) {
|
||||
"idm.internet.download.manager"
|
||||
} else if (isPackageInstalled("idm.internet.download.manager.adm.lite", context.packageManager)) {
|
||||
"idm.internet.download.manager.adm.lite"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
if (appName.isNotEmpty()) {
|
||||
val bundle = Bundle()
|
||||
defaultHeaders.forEach { a -> bundle.putString(a.key, a.value) }
|
||||
file.headers.forEach { a -> bundle.putString(a.key, a.value) }
|
||||
// documentation: https://www.apps2sd.info/idmp/faq?id=35
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
component = ComponentName(appName, "idm.internet.download.manager.Downloader")
|
||||
data = Uri.parse(file.url)
|
||||
putExtra("extra_headers", bundle)
|
||||
putExtra("extra_filename", notif)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
} else {
|
||||
ContextCompat.startActivity(
|
||||
context,
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("market://details?id=idm.internet.download.manager")
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
null
|
||||
)
|
||||
toast(currContext()?.getString(R.string.install_1dm))
|
||||
}
|
||||
}
|
||||
|
||||
private fun adm(context: Context, file: FileUrl, fileName: String, folder: String) {
|
||||
if (isPackageInstalled("com.dv.adm", context.packageManager)) {
|
||||
val bundle = Bundle()
|
||||
defaultHeaders.forEach { a -> bundle.putString(a.key, a.value) }
|
||||
file.headers.forEach { a -> bundle.putString(a.key, a.value) }
|
||||
// unofficial documentation: https://pastebin.com/ScDNr2if (there is no official documentation)
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
component = ComponentName("com.dv.adm", "com.dv.adm.AEditor")
|
||||
putExtra("com.dv.get.ACTION_LIST_ADD", "${file.url}<info>$fileName")
|
||||
putExtra("com.dv.get.ACTION_LIST_PATH", "${getDownloadDir(context)}$folder")
|
||||
putExtra("android.media.intent.extra.HTTP_HEADERS", bundle)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
} else {
|
||||
ContextCompat.startActivity(
|
||||
context,
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.dv.adm")).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
null
|
||||
)
|
||||
toast(currContext()?.getString(R.string.install_adm))
|
||||
}
|
||||
}
|
||||
}
|
36
app/src/main/java/ani/dantotsu/others/GlideApp.kt
Normal file
36
app/src/main/java/ani/dantotsu/others/GlideApp.kt
Normal file
|
@ -0,0 +1,36 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import ani.dantotsu.okHttpClient
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
@GlideModule
|
||||
class DantotsuGlideApp : AppGlideModule(){
|
||||
@SuppressLint("CheckResult")
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
super.applyOptions(context, builder)
|
||||
val diskCacheSizeBytes = 1024 * 1024 * 100 // 100 MiB
|
||||
builder.apply {
|
||||
setDiskCache(InternalCacheDiskCacheFactory(context, "img", diskCacheSizeBytes.toLong()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
registry.replace(
|
||||
GlideUrl::class.java,
|
||||
InputStream::class.java,
|
||||
OkHttpUrlLoader.Factory(okHttpClient)
|
||||
)
|
||||
super.registerComponents(context, glide, registry)
|
||||
}
|
||||
}
|
22
app/src/main/java/ani/dantotsu/others/Idiosyncrasy.kt
Normal file
22
app/src/main/java/ani/dantotsu/others/Idiosyncrasy.kt
Normal file
|
@ -0,0 +1,22 @@
|
|||
@file:Suppress("UNCHECKED_CAST", "DEPRECATION")
|
||||
|
||||
package ani.dantotsu.others
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import java.io.Serializable
|
||||
|
||||
inline fun <reified T : Serializable> Bundle.getSerialized(key: String): T? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
this.getSerializable(key, T::class.java)
|
||||
else
|
||||
this.getSerializable(key) as? T
|
||||
}
|
||||
|
||||
inline fun <reified T : Serializable> Intent.getSerialized(key: String): T? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
this.getSerializableExtra(key, T::class.java)
|
||||
else
|
||||
this.getSerializableExtra(key) as? T
|
||||
}
|
131
app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt
Normal file
131
app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt
Normal file
|
@ -0,0 +1,131 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.BottomSheetImageBinding
|
||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap
|
||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.saveImageToDownloads
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.shareImage
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ImageViewDialog : BottomSheetDialogFragment() {
|
||||
|
||||
private var _binding: BottomSheetImageBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var reload = false
|
||||
private var _title: String? = null
|
||||
private var _image: FileUrl? = null
|
||||
private var _image2: FileUrl? = null
|
||||
|
||||
var onReloadPressed: ((ImageViewDialog) -> Unit)? = null
|
||||
var trans1: List<BitmapTransformation>? = null
|
||||
var trans2: List<BitmapTransformation>? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
_title = it.getString("title")?.replace(Regex("[\\\\/:*?\"<>|]"), "")
|
||||
reload = it.getBoolean("reload")
|
||||
_image = it.getSerialized("image")!!
|
||||
_image2 = it.getSerialized("image2")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = BottomSheetImageBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val (title, image, image2) = Triple(_title, _image, _image2)
|
||||
if (image == null || title == null) {
|
||||
dismiss()
|
||||
snackString(getString(R.string.error_getting_image_data))
|
||||
return
|
||||
}
|
||||
if (reload) {
|
||||
binding.bottomImageReload.visibility = View.VISIBLE
|
||||
binding.bottomImageReload.setSafeOnClickListener {
|
||||
onReloadPressed?.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
binding.bottomImageTitle.text = title
|
||||
binding.bottomImageReload.setOnLongClickListener {
|
||||
openLinkInBrowser(image.url)
|
||||
if (image2 != null) openLinkInBrowser(image2.url)
|
||||
true
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
val binding = _binding ?: return@launch
|
||||
|
||||
var bitmap = requireContext().loadBitmap(image, trans1 ?: listOf())
|
||||
val bitmap2 = if (image2 != null) requireContext().loadBitmap(image2, trans2 ?: listOf()) else null
|
||||
|
||||
bitmap = if (bitmap2 != null && bitmap != null) mergeBitmap(bitmap, bitmap2,) else bitmap
|
||||
|
||||
if (bitmap != null) {
|
||||
binding.bottomImageShare.isEnabled = true
|
||||
binding.bottomImageSave.isEnabled = true
|
||||
binding.bottomImageSave.setOnClickListener {
|
||||
saveImageToDownloads(title, bitmap, requireActivity())
|
||||
}
|
||||
binding.bottomImageShare.setOnClickListener {
|
||||
shareImage(title, bitmap, requireContext())
|
||||
}
|
||||
|
||||
binding.bottomImageView.setImage(ImageSource.cachedBitmap(bitmap))
|
||||
ObjectAnimator.ofFloat(binding.bottomImageView, "alpha", 0f, 1f).setDuration(400L).start()
|
||||
binding.bottomImageProgress.visibility = View.GONE
|
||||
} else {
|
||||
toast(context?.getString(R.string.loading_image_failed))
|
||||
binding.bottomImageNo.visibility = View.VISIBLE
|
||||
binding.bottomImageProgress.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(title: String, image: FileUrl, showReload: Boolean = false, image2: FileUrl?) = ImageViewDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString("title", title)
|
||||
putBoolean("reload", showReload)
|
||||
putSerializable("image", image)
|
||||
putSerializable("image2", image2)
|
||||
}
|
||||
}
|
||||
|
||||
fun newInstance(activity: FragmentActivity, title: String?, image: String?): Boolean {
|
||||
ImageViewDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString("title", title ?: return false)
|
||||
putSerializable("image", FileUrl(image ?: return false))
|
||||
}
|
||||
show(activity.supportFragmentManager, "image")
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
59
app/src/main/java/ani/dantotsu/others/Jikan.kt
Normal file
59
app/src/main/java/ani/dantotsu/others/Jikan.kt
Normal file
|
@ -0,0 +1,59 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
object Jikan {
|
||||
|
||||
const val apiUrl = "https://api.jikan.moe/v4/"
|
||||
|
||||
suspend inline fun <reified T : Any> query(endpoint: String): T? {
|
||||
return tryWithSuspend { client.get("$apiUrl$endpoint").parsed() }
|
||||
}
|
||||
|
||||
suspend fun getEpisodes(malId: Int): Map<String, Episode> {
|
||||
var hasNextPage = true
|
||||
var page = 0
|
||||
val eps = mutableMapOf<String, Episode>()
|
||||
while (hasNextPage) {
|
||||
page++
|
||||
val res = query<EpisodeResponse>("anime/$malId/episodes?page=$page")
|
||||
res?.data?.forEach {
|
||||
val ep = it.malID.toString()
|
||||
eps[ep] = Episode(ep, title = it.title,
|
||||
//Personal revenge with 34566 :prayge:
|
||||
filler = if(malId!=34566) it.filler else true
|
||||
)
|
||||
}
|
||||
hasNextPage = res?.pagination?.hasNextPage == true
|
||||
}
|
||||
return eps
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class EpisodeResponse(
|
||||
val pagination: Pagination? = null,
|
||||
val data: List<Datum>? = null
|
||||
) {
|
||||
@Serializable
|
||||
data class Datum(
|
||||
@SerialName("mal_id")
|
||||
val malID: Int,
|
||||
val title: String? = null,
|
||||
val filler: Boolean,
|
||||
// val recap: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Pagination(
|
||||
@SerialName("has_next_page")
|
||||
val hasNextPage: Boolean? = null
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
206
app/src/main/java/ani/dantotsu/others/JsUnpacker.kt
Normal file
206
app/src/main/java/ani/dantotsu/others/JsUnpacker.kt
Normal file
|
@ -0,0 +1,206 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import ani.dantotsu.logger
|
||||
import java.util.regex.*
|
||||
import kotlin.math.pow
|
||||
|
||||
// https://github.com/cylonu87/JsUnpacker
|
||||
// https://github.com/recloudstream/cloudstream/blob/master/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt
|
||||
|
||||
class JsUnpacker(packedJS: String?) {
|
||||
private var packedJS: String? = null
|
||||
|
||||
/**
|
||||
* Detects whether the javascript is P.A.C.K.E.R. coded.
|
||||
*
|
||||
* @return true if it's P.A.C.K.E.R. coded.
|
||||
*/
|
||||
fun detect(): Boolean {
|
||||
val js = packedJS!!.replace(" ", "")
|
||||
val p = Pattern.compile("eval\\(function\\(p,a,c,k,e,[rd]")
|
||||
val m = p.matcher(js)
|
||||
return m.find()
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack the javascript
|
||||
*
|
||||
* @return the javascript unpacked or null.
|
||||
*/
|
||||
fun unpack(): String? {
|
||||
val js = packedJS ?: return null
|
||||
try {
|
||||
var p =
|
||||
Pattern.compile("""\}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)""", Pattern.DOTALL)
|
||||
var m = p.matcher(js)
|
||||
if (m.find() && m.groupCount() == 4) {
|
||||
val payload = m.group(1)?.replace("\\'", "'") ?: return null
|
||||
val tabs = m.group(4)?.split("\\|".toRegex())?.toTypedArray() ?: return null
|
||||
val radix = m.group(2)?.toIntOrNull() ?: 36
|
||||
val count = m.group(3)?.toIntOrNull() ?: 0
|
||||
if (tabs.size != count) {
|
||||
throw Exception("Unknown p.a.c.k.e.r. encoding")
|
||||
}
|
||||
|
||||
val un = Unbase(radix)
|
||||
p = Pattern.compile("\\b\\w+\\b")
|
||||
m = p.matcher(payload)
|
||||
val decoded = StringBuilder(payload)
|
||||
var replaceOffset = 0
|
||||
while (m.find()) {
|
||||
val word = m.group(0) ?: continue
|
||||
val x = un.unbase(word)
|
||||
val value = if (x < tabs.size && x >= 0) {
|
||||
tabs[x]
|
||||
} else null
|
||||
if (!value.isNullOrEmpty()) {
|
||||
decoded.replace(m.start() + replaceOffset, m.end() + replaceOffset, value)
|
||||
replaceOffset += value.length - word.length
|
||||
}
|
||||
}
|
||||
return decoded.toString()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger(e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private inner class Unbase(private val radix: Int) {
|
||||
private val a62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
private val a95 =
|
||||
" !\"#$%&\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
|
||||
private var alphabet: String? = null
|
||||
private var dictionary: HashMap<String, Int>? = null
|
||||
fun unbase(str: String): Int {
|
||||
var ret = 0
|
||||
if (alphabet == null) {
|
||||
ret = str.toInt(radix)
|
||||
} else {
|
||||
val tmp = StringBuilder(str).reverse().toString()
|
||||
for (i in tmp.indices) {
|
||||
ret += (radix.toDouble().pow(i.toDouble()) * dictionary!![tmp.substring(i, i + 1)]!!).toInt()
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
init {
|
||||
if (radix > 36) {
|
||||
when {
|
||||
radix < 62 -> {
|
||||
alphabet = a62.substring(0, radix)
|
||||
}
|
||||
radix in 63..94 -> {
|
||||
alphabet = a95.substring(0, radix)
|
||||
}
|
||||
radix == 62 -> {
|
||||
alphabet = a62
|
||||
}
|
||||
radix == 95 -> {
|
||||
alphabet = a95
|
||||
}
|
||||
}
|
||||
dictionary = HashMap(95)
|
||||
for (i in 0 until alphabet!!.length) {
|
||||
dictionary!![alphabet!!.substring(i, i + 1)] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
this.packedJS = packedJS
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
val c =
|
||||
listOf(
|
||||
0x63,
|
||||
0x6f,
|
||||
0x6d,
|
||||
0x2e,
|
||||
0x67,
|
||||
0x6f,
|
||||
0x6f,
|
||||
0x67,
|
||||
0x6c,
|
||||
0x65,
|
||||
0x2e,
|
||||
0x61,
|
||||
0x6e,
|
||||
0x64,
|
||||
0x72,
|
||||
0x6f,
|
||||
0x69,
|
||||
0x64,
|
||||
0x2e,
|
||||
0x67,
|
||||
0x6d,
|
||||
0x73,
|
||||
0x2e,
|
||||
0x61,
|
||||
0x64,
|
||||
0x73,
|
||||
0x2e,
|
||||
0x4d,
|
||||
0x6f,
|
||||
0x62,
|
||||
0x69,
|
||||
0x6c,
|
||||
0x65,
|
||||
0x41,
|
||||
0x64,
|
||||
0x73
|
||||
)
|
||||
private val z =
|
||||
listOf(
|
||||
0x63,
|
||||
0x6f,
|
||||
0x6d,
|
||||
0x2e,
|
||||
0x66,
|
||||
0x61,
|
||||
0x63,
|
||||
0x65,
|
||||
0x62,
|
||||
0x6f,
|
||||
0x6f,
|
||||
0x6b,
|
||||
0x2e,
|
||||
0x61,
|
||||
0x64,
|
||||
0x73,
|
||||
0x2e,
|
||||
0x41,
|
||||
0x64
|
||||
)
|
||||
|
||||
fun String.load(): String? {
|
||||
return try {
|
||||
var load = this
|
||||
|
||||
for (q in c.indices) {
|
||||
if (c[q % 4] > 270) {
|
||||
load += c[q % 3]
|
||||
} else {
|
||||
load += c[q].toChar()
|
||||
}
|
||||
}
|
||||
|
||||
Class.forName(load.substring(load.length - c.size, load.length)).name
|
||||
} catch (_: Exception) {
|
||||
try {
|
||||
var f = c[2].toChar().toString()
|
||||
for (w in z.indices) {
|
||||
f += z[w].toChar()
|
||||
}
|
||||
return Class.forName(f.substring(0b001, f.length)).name
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
app/src/main/java/ani/dantotsu/others/Kitsu.kt
Normal file
111
app/src/main/java/ani/dantotsu/others/Kitsu.kt
Normal file
|
@ -0,0 +1,111 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
object Kitsu {
|
||||
private suspend fun getKitsuData(query: String): KitsuResponse? {
|
||||
val headers = mapOf(
|
||||
"Content-Type" to "application/json",
|
||||
"Accept" to "application/json",
|
||||
"Connection" to "keep-alive",
|
||||
"DNT" to "1",
|
||||
"Origin" to "https://kitsu.io"
|
||||
)
|
||||
val json = tryWithSuspend { client.post("https://kitsu.io/api/graphql", headers, data = mapOf("query" to query)) }
|
||||
return json?.parsed()
|
||||
}
|
||||
|
||||
suspend fun getKitsuEpisodesDetails(media: Media): Map<String, Episode>? {
|
||||
val print = false
|
||||
logger("Kitsu : title=${media.mainName()}", print)
|
||||
val query =
|
||||
"""
|
||||
query {
|
||||
lookupMapping(externalId: ${media.id}, externalSite: ANILIST_ANIME) {
|
||||
__typename
|
||||
... on Anime {
|
||||
id
|
||||
episodes(first: 2000) {
|
||||
nodes {
|
||||
number
|
||||
titles {
|
||||
canonical
|
||||
}
|
||||
description
|
||||
thumbnail {
|
||||
original {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
val result = getKitsuData(query) ?: return null
|
||||
logger("Kitsu : result=$result", print)
|
||||
media.idKitsu = result.data?.lookupMapping?.id
|
||||
return (result.data?.lookupMapping?.episodes?.nodes?:return null).mapNotNull { ep ->
|
||||
val num = ep?.num?.toString()?:return@mapNotNull null
|
||||
num to Episode(
|
||||
number = num,
|
||||
title = ep.titles?.canonical,
|
||||
desc = ep.description?.en,
|
||||
thumb = FileUrl[ep.thumbnail?.original?.url],
|
||||
)
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class KitsuResponse(
|
||||
@SerialName("data") val data: Data? = null
|
||||
) {
|
||||
@Serializable
|
||||
data class Data (
|
||||
@SerialName("lookupMapping") val lookupMapping: LookupMapping? = null
|
||||
)
|
||||
@Serializable
|
||||
data class LookupMapping (
|
||||
@SerialName("id") val id: String? = null,
|
||||
@SerialName("episodes") val episodes: Episodes? = null
|
||||
)
|
||||
@Serializable
|
||||
data class Episodes (
|
||||
@SerialName("nodes") val nodes: List<Node?>? = null
|
||||
)
|
||||
@Serializable
|
||||
data class Node (
|
||||
@SerialName("number") val num: Long? = null,
|
||||
@SerialName("titles") val titles: Titles? = null,
|
||||
@SerialName("description") val description: Description? = null,
|
||||
@SerialName("thumbnail") val thumbnail: Thumbnail? = null
|
||||
)
|
||||
@Serializable
|
||||
data class Description (
|
||||
@SerialName("en") val en: String? = null
|
||||
)
|
||||
@Serializable
|
||||
data class Thumbnail (
|
||||
@SerialName("original") val original: Original? = null
|
||||
)
|
||||
@Serializable
|
||||
data class Original (
|
||||
@SerialName("url") val url: String? = null
|
||||
)
|
||||
@Serializable
|
||||
data class Titles (
|
||||
@SerialName("canonical") val canonical: String? = null
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
50
app/src/main/java/ani/dantotsu/others/MalScraper.kt
Normal file
50
app/src/main/java/ani/dantotsu/others/MalScraper.kt
Normal file
|
@ -0,0 +1,50 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.snackString
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.withTimeout
|
||||
|
||||
object MalScraper {
|
||||
private val headers = mapOf(
|
||||
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36"
|
||||
)
|
||||
|
||||
suspend fun loadMedia(media: Media) {
|
||||
try {
|
||||
withTimeout(6000) {
|
||||
if (media.anime != null) {
|
||||
val res = client.get("https://myanimelist.net/anime/${media.idMAL}", headers).document
|
||||
val a = res.select(".title-english").text()
|
||||
media.nameMAL = if (a != "") a else res.select(".title-name").text()
|
||||
media.typeMAL =
|
||||
if (res.select("div.spaceit_pad > a").isNotEmpty()) res.select("div.spaceit_pad > a")[0].text() else null
|
||||
media.anime.op = arrayListOf()
|
||||
res.select(".opnening > table > tbody > tr").forEach {
|
||||
val text = it.text()
|
||||
if (!text.contains("Help improve our database"))
|
||||
media.anime.op.add(it.text())
|
||||
}
|
||||
media.anime.ed = arrayListOf()
|
||||
res.select(".ending > table > tbody > tr").forEach {
|
||||
val text = it.text()
|
||||
if (!text.contains("Help improve our database"))
|
||||
media.anime.ed.add(it.text())
|
||||
}
|
||||
} else {
|
||||
val res = client.get("https://myanimelist.net/manga/${media.idMAL}", headers).document
|
||||
val b = res.select(".title-english").text()
|
||||
val a = res.select(".h1-title").text().removeSuffix(b)
|
||||
media.nameMAL = a
|
||||
media.typeMAL =
|
||||
if (res.select("div.spaceit_pad > a").isNotEmpty()) res.select("div.spaceit_pad > a")[0].text() else null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is TimeoutCancellationException) snackString(currContext()?.getString(R.string.error_loading_mal_data))
|
||||
}
|
||||
}
|
||||
}
|
45
app/src/main/java/ani/dantotsu/others/MalSyncBackup.kt
Normal file
45
app/src/main/java/ani/dantotsu/others/MalSyncBackup.kt
Normal file
|
@ -0,0 +1,45 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
object MalSyncBackup {
|
||||
@Serializable
|
||||
data class MalBackUpSync(
|
||||
@SerialName("Pages") val pages: Map<String, Map<String, Page>>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Page(
|
||||
val identifier: String,
|
||||
val title: String,
|
||||
val url: String? = null,
|
||||
val image: String? = null,
|
||||
val active: Boolean? = null,
|
||||
)
|
||||
|
||||
suspend fun get(id: Int, name: String, dub: Boolean = false): ShowResponse? {
|
||||
return tryWithSuspend {
|
||||
val json =
|
||||
client.get("https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/anilist/anime/$id.json")
|
||||
if (json.text != "404: Not Found")
|
||||
json.parsed<MalBackUpSync>().pages?.get(name)?.forEach {
|
||||
val page = it.value
|
||||
val isDub = page.title.lowercase().replace(" ", "").endsWith("(dub)")
|
||||
val slug = if (dub == isDub) page.identifier else null
|
||||
if (slug != null && page.active == true && page.url != null) {
|
||||
val url = when(name){
|
||||
"Gogoanime" -> slug
|
||||
"Tenshi" -> slug
|
||||
else -> page.url
|
||||
}
|
||||
return@tryWithSuspend ShowResponse(page.title, url, page.image ?: "")
|
||||
}
|
||||
}
|
||||
return@tryWithSuspend null
|
||||
}
|
||||
}
|
||||
}
|
86
app/src/main/java/ani/dantotsu/others/OutlineTextView.kt
Normal file
86
app/src/main/java/ani/dantotsu/others/OutlineTextView.kt
Normal file
|
@ -0,0 +1,86 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import ani.dantotsu.R
|
||||
|
||||
class OutlineTextView : AppCompatTextView {
|
||||
|
||||
private val defaultStrokeWidth = 0F
|
||||
private var isDrawing: Boolean = false
|
||||
|
||||
private var strokeColor: Int = 0
|
||||
private var strokeWidth: Float = 0.toFloat()
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
initResources(context, null)
|
||||
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
||||
initResources(context, attrs)
|
||||
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
initResources(context, attrs)
|
||||
|
||||
}
|
||||
|
||||
private fun initResources(context: Context?, attrs: AttributeSet?) {
|
||||
if (attrs != null) {
|
||||
val a = context?.obtainStyledAttributes(attrs, R.styleable.OutlineTextView)
|
||||
strokeColor = a!!.getColor(
|
||||
R.styleable.OutlineTextView_outlineColor,
|
||||
currentTextColor
|
||||
)
|
||||
strokeWidth = a.getFloat(
|
||||
R.styleable.OutlineTextView_outlineWidth,
|
||||
defaultStrokeWidth
|
||||
)
|
||||
|
||||
a.recycle()
|
||||
} else {
|
||||
strokeColor = currentTextColor
|
||||
strokeWidth = defaultStrokeWidth
|
||||
}
|
||||
setStrokeWidth(strokeWidth)
|
||||
}
|
||||
|
||||
|
||||
private fun setStrokeWidth(width: Float) {
|
||||
strokeWidth = width.toPx(context)
|
||||
}
|
||||
|
||||
private fun Float.toPx(context: Context) = (this * context.resources.displayMetrics.scaledDensity + 0.5F)
|
||||
|
||||
override fun invalidate() {
|
||||
if (isDrawing) return
|
||||
super.invalidate()
|
||||
}
|
||||
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
if (strokeWidth > 0) {
|
||||
isDrawing = true
|
||||
super.onDraw(canvas)
|
||||
|
||||
paint.style = Paint.Style.STROKE
|
||||
paint.strokeWidth = strokeWidth
|
||||
val colorTmp = paint.color
|
||||
setTextColor(strokeColor)
|
||||
super.onDraw(canvas)
|
||||
|
||||
setTextColor(colorTmp)
|
||||
paint.style = Paint.Style.FILL
|
||||
|
||||
isDrawing = false
|
||||
} else {
|
||||
super.onDraw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
27
app/src/main/java/ani/dantotsu/others/ResettableTimer.kt
Normal file
27
app/src/main/java/ani/dantotsu/others/ResettableTimer.kt
Normal file
|
@ -0,0 +1,27 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.*
|
||||
|
||||
class ResettableTimer {
|
||||
var resetLock = AtomicBoolean(false)
|
||||
var timer = Timer()
|
||||
fun reset(timerTask: TimerTask, delay: Long) {
|
||||
if (!resetLock.getAndSet(true)) {
|
||||
timer.cancel()
|
||||
timer.purge()
|
||||
timer = Timer()
|
||||
timer.schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
if (!resetLock.getAndSet(true)) {
|
||||
timerTask.run()
|
||||
timer.cancel()
|
||||
timer.purge()
|
||||
resetLock.set(false)
|
||||
}
|
||||
}
|
||||
}, delay)
|
||||
resetLock.set(false)
|
||||
}
|
||||
}
|
||||
}
|
84
app/src/main/java/ani/dantotsu/others/SpoilerPlugin.kt
Normal file
84
app/src/main/java/ani/dantotsu/others/SpoilerPlugin.kt
Normal file
|
@ -0,0 +1,84 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import android.graphics.Color
|
||||
import android.text.Spannable
|
||||
import android.text.Spanned
|
||||
import android.text.TextPaint
|
||||
import android.text.style.CharacterStyle
|
||||
import android.text.style.ClickableSpan
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import io.noties.markwon.AbstractMarkwonPlugin
|
||||
import io.noties.markwon.utils.ColorUtils
|
||||
import java.util.regex.*
|
||||
|
||||
class SpoilerPlugin : AbstractMarkwonPlugin() {
|
||||
override fun beforeSetText(textView: TextView, markdown: Spanned) {
|
||||
applySpoilerSpans(markdown as Spannable)
|
||||
}
|
||||
|
||||
private class RedditSpoilerSpan : CharacterStyle() {
|
||||
private var revealed = false
|
||||
override fun updateDrawState(tp: TextPaint) {
|
||||
if (!revealed) {
|
||||
// use the same text color
|
||||
tp.bgColor = Color.DKGRAY
|
||||
tp.color = Color.DKGRAY
|
||||
} else {
|
||||
// for example keep a bit of black background to remind that it is a spoiler
|
||||
tp.bgColor = ColorUtils.applyAlpha(Color.DKGRAY, 25)
|
||||
}
|
||||
}
|
||||
|
||||
fun setRevealed(revealed: Boolean) {
|
||||
this.revealed = revealed
|
||||
}
|
||||
}
|
||||
|
||||
// we also could make text size smaller (but then MetricAffectingSpan should be used)
|
||||
private class HideSpoilerSyntaxSpan : CharacterStyle() {
|
||||
override fun updateDrawState(tp: TextPaint) {
|
||||
// set transparent color
|
||||
tp.color = 0
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val RE = Pattern.compile("~!.+?!~")
|
||||
private fun applySpoilerSpans(spannable: Spannable) {
|
||||
val text = spannable.toString()
|
||||
val matcher = RE.matcher(text)
|
||||
while (matcher.find()) {
|
||||
val spoilerSpan = RedditSpoilerSpan()
|
||||
val clickableSpan: ClickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
spoilerSpan.setRevealed(true)
|
||||
widget.postInvalidateOnAnimation()
|
||||
}
|
||||
|
||||
override fun updateDrawState(ds: TextPaint) {
|
||||
// no op
|
||||
}
|
||||
}
|
||||
val s = matcher.start()
|
||||
val e = matcher.end()
|
||||
spannable.setSpan(spoilerSpan, s, e, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
spannable.setSpan(clickableSpan, s, e, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
// we also can hide original syntax
|
||||
spannable.setSpan(
|
||||
HideSpoilerSyntaxSpan(),
|
||||
s,
|
||||
s + 2,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
spannable.setSpan(
|
||||
HideSpoilerSyntaxSpan(),
|
||||
e - 2,
|
||||
e,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
66
app/src/main/java/ani/dantotsu/others/Xpandable.kt
Normal file
66
app/src/main/java/ani/dantotsu/others/Xpandable.kt
Normal file
|
@ -0,0 +1,66 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.view.children
|
||||
import ani.dantotsu.R
|
||||
|
||||
|
||||
class Xpandable @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : LinearLayout(context, attrs) {
|
||||
var expanded: Boolean = false
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs, R.styleable.Xpandable) {
|
||||
expanded = getBoolean(R.styleable.Xpandable_isExpanded, expanded)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
|
||||
getChildAt(0)!!.setOnClickListener {
|
||||
if (expanded) hideAll() else showAll()
|
||||
postDelayed({
|
||||
expanded = !expanded
|
||||
}, 300)
|
||||
}
|
||||
|
||||
if(!expanded) children.forEach {
|
||||
if (it != getChildAt(0)){
|
||||
it.visibility = GONE
|
||||
}
|
||||
}
|
||||
super.onAttachedToWindow()
|
||||
}
|
||||
|
||||
|
||||
private fun hideAll() {
|
||||
children.forEach {
|
||||
if (it != getChildAt(0)){
|
||||
ObjectAnimator.ofFloat(it, "scaleY", 1f, 0.5f).setDuration(200).start()
|
||||
ObjectAnimator.ofFloat(it, "translationY", 0f, -32f).setDuration(200).start()
|
||||
ObjectAnimator.ofFloat(it, "alpha", 1f, 0f).setDuration(200).start()
|
||||
it.postDelayed({
|
||||
it.visibility = GONE
|
||||
}, 300)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAll() {
|
||||
children.forEach {
|
||||
if (it != getChildAt(0)){
|
||||
it.visibility = VISIBLE
|
||||
ObjectAnimator.ofFloat(it, "scaleY", 0.5f, 1f).setDuration(200).start()
|
||||
ObjectAnimator.ofFloat(it, "translationY", -32f, 0f).setDuration(200).start()
|
||||
ObjectAnimator.ofFloat(it, "alpha", 0f, 1f).setDuration(200).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package ani.dantotsu.others.imagesearch
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.App.Companion.context
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.ActivityImageSearchBinding
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ImageSearchActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityImageSearchBinding
|
||||
private val viewModel: ImageSearchViewModel by viewModels()
|
||||
|
||||
private val imageSelectionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||
uri?.let { imageUri ->
|
||||
val contentResolver = applicationContext.contentResolver
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
val inputStream = contentResolver.openInputStream(imageUri)
|
||||
|
||||
if(inputStream != null) viewModel.analyzeImage(inputStream)
|
||||
else toast(getString(R.string.error_loading_image))
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityImageSearchBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.uploadImage.setOnClickListener {
|
||||
viewModel.clearResults()
|
||||
imageSelectionLauncher.launch("image/*")
|
||||
}
|
||||
binding.imageSearchTitle.setOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
viewModel.searchResultLiveData.observe(this) { result ->
|
||||
result?.let { displayResult(it) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun displayResult(result: ImageSearchViewModel.SearchResult) {
|
||||
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
|
||||
val searchResults: List<ImageSearchViewModel.ImageResult> = result.result.orEmpty()
|
||||
val adapter = ImageSearchResultAdapter(searchResults)
|
||||
|
||||
adapter.setOnItemClickListener(object : ImageSearchResultAdapter.OnItemClickListener {
|
||||
override fun onItemClick(searchResult: ImageSearchViewModel.ImageResult) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val id = searchResult.anilist?.id?.toInt()
|
||||
if (id==null){
|
||||
toast(getString(R.string.no_anilist_id_found))
|
||||
return@launch
|
||||
}
|
||||
val media = Anilist.query.getMedia(id, false)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
media?.let {
|
||||
startActivity(
|
||||
Intent(this@ImageSearchActivity, MediaDetailsActivity::class.java)
|
||||
.putExtra("media", it)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
recyclerView.post {
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package ani.dantotsu.others.imagesearch
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ItemSearchByImageBinding
|
||||
import ani.dantotsu.loadImage
|
||||
|
||||
class ImageSearchResultAdapter(private val searchResults: List<ImageSearchViewModel.ImageResult>) :
|
||||
RecyclerView.Adapter<ImageSearchResultAdapter.SearchResultViewHolder>() {
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(searchResult: ImageSearchViewModel.ImageResult)
|
||||
}
|
||||
|
||||
private var itemClickListener: OnItemClickListener? = null
|
||||
|
||||
fun setOnItemClickListener(listener: OnItemClickListener) {
|
||||
itemClickListener = listener
|
||||
}
|
||||
|
||||
inner class SearchResultViewHolder(val binding : ItemSearchByImageBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchResultViewHolder {
|
||||
val binding = ItemSearchByImageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SearchResultViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SearchResultViewHolder, position: Int) {
|
||||
val searchResult = searchResults[position]
|
||||
val binding = holder.binding
|
||||
binding.root.setOnClickListener {
|
||||
itemClickListener?.onItemClick(searchResult)
|
||||
}
|
||||
|
||||
binding.root.context.apply {
|
||||
binding.itemCompactTitle.text = searchResult.anilist?.title?.romaji
|
||||
binding.itemTotal.text = getString(
|
||||
R.string.similarity_text, String.format("%.1f", searchResult.similarity?.times(100))
|
||||
)
|
||||
|
||||
binding.episodeNumber.text = getString(R.string.episode_num, searchResult.episode.toString())
|
||||
binding.timeStamp.text = getString(
|
||||
R.string.time_range,
|
||||
toTimestamp(searchResult.from),
|
||||
toTimestamp(searchResult.to)
|
||||
)
|
||||
|
||||
binding.itemImage.loadImage(searchResult.image)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return searchResults.size
|
||||
}
|
||||
|
||||
private fun toTimestamp(seconds: Double?): String {
|
||||
val minutes = (seconds?.div(60))?.toInt()
|
||||
val remainingSeconds = (seconds?.mod(60.0))?.toInt()
|
||||
|
||||
val minutesString = minutes.toString().padStart(2, '0')
|
||||
val secondsString = remainingSeconds.toString().padStart(2, '0')
|
||||
|
||||
return "$minutesString:$secondsString"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package ani.dantotsu.others.imagesearch
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.client
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.InputStream
|
||||
|
||||
class ImageSearchViewModel : ViewModel() {
|
||||
val searchResultLiveData: MutableLiveData<SearchResult> = MutableLiveData()
|
||||
|
||||
private val url = "https://api.trace.moe/search?cutBorders&anilistInfo"
|
||||
|
||||
suspend fun analyzeImage(inputStream: InputStream) {
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart(
|
||||
"image",
|
||||
"image.jpg",
|
||||
inputStream.readBytes().toRequestBody("image/jpeg".toMediaType())
|
||||
)
|
||||
.build()
|
||||
|
||||
val res = client.post(url, requestBody = requestBody).parsed<SearchResult>()
|
||||
searchResultLiveData.postValue(res)
|
||||
}
|
||||
|
||||
fun clearResults(){
|
||||
searchResultLiveData.postValue(SearchResult())
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SearchResult(
|
||||
val frameCount: Long? = null,
|
||||
val error: String? = null,
|
||||
val result: List<ImageResult>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ImageResult(
|
||||
val anilist: AnilistData? = null,
|
||||
val filename: String? = null,
|
||||
@SerialName("episode") val rawEpisode: JsonElement? = null,
|
||||
val from: Double? = null,
|
||||
val to: Double? = null,
|
||||
val similarity: Double? = null,
|
||||
val video: String? = null,
|
||||
val image: String? = null
|
||||
) {
|
||||
val episode: String?
|
||||
get() = rawEpisode?.toString()
|
||||
|
||||
override fun toString(): String {
|
||||
return "$image & $video"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AnilistData(
|
||||
val id: Long? = null,
|
||||
val idMal: Long? = null,
|
||||
val title: Title? = null,
|
||||
val synonyms: List<String>? = null,
|
||||
val isAdult: Boolean? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Title(
|
||||
val native: String? = null,
|
||||
val romaji: String? = null,
|
||||
val english: String? = null
|
||||
)
|
||||
}
|
27
app/src/main/java/ani/dantotsu/others/webview/CloudFlare.kt
Normal file
27
app/src/main/java/ani/dantotsu/others/webview/CloudFlare.kt
Normal file
|
@ -0,0 +1,27 @@
|
|||
package ani.dantotsu.others.webview
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import ani.dantotsu.FileUrl
|
||||
|
||||
class CloudFlare(override val location: FileUrl) : WebViewBottomDialog() {
|
||||
val cfTag = "cf_clearance"
|
||||
|
||||
override var title = "Cloudflare Bypass"
|
||||
override val webViewClient = object : WebViewClient() {
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
val cookie = cookies.getCookie(url.toString())
|
||||
if (cookie?.contains(cfTag) == true) {
|
||||
val clearance = cookie.substringAfter("$cfTag=").substringBefore(";")
|
||||
privateCallback.invoke(mapOf(cfTag to clearance))
|
||||
}
|
||||
super.onPageStarted(view, url, favicon)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(url: FileUrl) = CloudFlare(url)
|
||||
fun newInstance(url: String) = CloudFlare(FileUrl(url))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package ani.dantotsu.others.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebViewClient
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.databinding.BottomSheetWebviewBinding
|
||||
import ani.dantotsu.defaultHeaders
|
||||
|
||||
abstract class WebViewBottomDialog : BottomSheetDialogFragment() {
|
||||
|
||||
abstract val location: FileUrl
|
||||
|
||||
private var _binding: BottomSheetWebviewBinding? = null
|
||||
open val binding get() = _binding!!
|
||||
|
||||
abstract val title: String
|
||||
abstract val webViewClient: WebViewClient
|
||||
|
||||
var callback: ((Map<String, String>) -> Unit)? = null
|
||||
|
||||
protected var privateCallback: ((Map<String, String>) -> Unit) = {
|
||||
callback?.invoke(it)
|
||||
_binding?.webView?.stopLoading()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
val cookies: CookieManager = CookieManager.getInstance()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = BottomSheetWebviewBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.webViewTitle.text = title
|
||||
binding.webView.settings.apply {
|
||||
javaScriptEnabled = true
|
||||
userAgentString = defaultHeaders["User-Agent"]
|
||||
}
|
||||
cookies.setAcceptThirdPartyCookies(binding.webView, true)
|
||||
binding.webView.webViewClient = webViewClient
|
||||
binding.webView.loadUrl(location.url, location.headers)
|
||||
this.dismiss()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue