Initial commit

This commit is contained in:
Finnley Somdahl 2023-10-17 18:42:43 -05:00
commit 21bfbfb139
520 changed files with 47819 additions and 0 deletions

View file

@ -0,0 +1 @@
DisabledReports.kt

View 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
)
}

View 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
}
}
}

View 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()
}
}

View 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))
}
}
}

View 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)
}
}

View 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
}

View 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
}
}
}

View 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
)
}
}

View 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
}
}
}
}
}

View 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
)
}
}

View 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))
}
}
}

View 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
}
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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
)
}
}
}
}

View 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()
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -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"
}
}

View file

@ -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
)
}

View 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))
}
}

View file

@ -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()
}
}