parent acb0225699
author Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> 1698992132 -0500 committer Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> 1698992691 -0500 manga downloading base Update README.md Update README.md Update README.md Update README.md Update README.md
This commit is contained in:
parent
acb0225699
commit
20acd71b1a
24 changed files with 763 additions and 64 deletions
|
@ -267,6 +267,8 @@
|
|||
|
||||
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".download.manga.MangaDownloaderService" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -23,11 +23,14 @@ import uy.kohesive.injekt.api.InjektRegistrar
|
|||
import uy.kohesive.injekt.api.addSingleton
|
||||
import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
|
||||
class AppModule(val app: Application) : InjektModule {
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
|
||||
addSingletonFactory { DownloadsManager(app) }
|
||||
|
||||
addSingletonFactory { NetworkHelper(app, get()) }
|
||||
|
||||
addSingletonFactory { AnimeExtensionManager(app) }
|
||||
|
|
115
app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
Normal file
115
app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
Normal file
|
@ -0,0 +1,115 @@
|
|||
package ani.dantotsu.download
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
|
||||
class DownloadsManager(private val context: Context) {
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences("downloads_pref", Context.MODE_PRIVATE)
|
||||
private val gson = Gson()
|
||||
private val downloadsList = loadDownloads().toMutableList()
|
||||
|
||||
val mangaDownloads: List<Download>
|
||||
get() = downloadsList.filter { it.type == Download.Type.MANGA }
|
||||
val animeDownloads: List<Download>
|
||||
get() = downloadsList.filter { it.type == Download.Type.ANIME }
|
||||
|
||||
private fun saveDownloads() {
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
prefs.edit().putString("downloads_key", jsonString).apply()
|
||||
}
|
||||
|
||||
private fun loadDownloads(): List<Download> {
|
||||
val jsonString = prefs.getString("downloads_key", null)
|
||||
return if (jsonString != null) {
|
||||
val type = object : TypeToken<List<Download>>() {}.type
|
||||
gson.fromJson(jsonString, type)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun addDownload(download: Download) {
|
||||
downloadsList.add(download)
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeDownload(download: Download) {
|
||||
downloadsList.remove(download)
|
||||
removeDirectory(download)
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
private fun removeDirectory(download: Download) {
|
||||
val directory = if (download.type == Download.Type.MANGA){
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime/${download.title}/${download.chapter}")
|
||||
}
|
||||
|
||||
// Check if the directory exists and delete it recursively
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun exportDownloads(download: Download) { //copies to the downloads folder available to the user
|
||||
val directory = if (download.type == Download.Type.MANGA){
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime/${download.title}/${download.chapter}")
|
||||
}
|
||||
val destination = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/${download.title}/${download.chapter}")
|
||||
if (directory.exists()) {
|
||||
val copied = directory.copyRecursively(destination, true)
|
||||
if (copied) {
|
||||
Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun purgeDownloads(type: Download.Type){
|
||||
val directory = if (type == Download.Type.MANGA){
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
downloadsList.removeAll { it.type == type }
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class Download(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||
enum class Type {
|
||||
MANGA,
|
||||
ANIME
|
||||
}
|
||||
}
|
|
@ -0,0 +1,273 @@
|
|||
package ani.dantotsu.download.manga
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||
import ani.dantotsu.snackString
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class MangaDownloaderService : Service() {
|
||||
|
||||
private var title: String = ""
|
||||
private var chapter: String = ""
|
||||
private var retries: Int = 2
|
||||
private var simultaneousDownloads: Int = 2
|
||||
private var imageData: List<ImageData> = listOf()
|
||||
private var sourceMedia: Media? = null
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
private lateinit var builder: NotificationCompat.Builder
|
||||
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// This is only required for bound services.
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||
setContentTitle("Manga Download Progress")
|
||||
setContentText("Downloading $title - $chapter")
|
||||
setSmallIcon(R.drawable.ic_round_download_24)
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setOnlyAlertOnce(true)
|
||||
setProgress(0, 0, false)
|
||||
}
|
||||
startForeground(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
snackString("Download started")
|
||||
title = intent?.getStringExtra("title") ?: ""
|
||||
chapter = intent?.getStringExtra("chapter") ?: ""
|
||||
retries = intent?.getIntExtra("retries", 2) ?: 2
|
||||
simultaneousDownloads = intent?.getIntExtra("simultaneousDownloads", 2) ?: 2
|
||||
imageData = ServiceDataSingleton.imageData
|
||||
sourceMedia = ServiceDataSingleton.sourceMedia
|
||||
ServiceDataSingleton.imageData = listOf()
|
||||
ServiceDataSingleton.sourceMedia = null
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
download()
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
suspend fun download() {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this@MangaDownloaderService,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
Toast.makeText(
|
||||
this@MangaDownloaderService,
|
||||
"Please grant notification permission",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return@withContext
|
||||
}
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
|
||||
val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
||||
|
||||
// Loop through each ImageData object
|
||||
var farthest = 0
|
||||
for ((index, image) in imageData.withIndex()) {
|
||||
// Limit the number of simultaneous downloads
|
||||
if (deferredList.size >= simultaneousDownloads) {
|
||||
// Wait for all deferred to complete and clear the list
|
||||
deferredList.awaitAll()
|
||||
deferredList.clear()
|
||||
}
|
||||
|
||||
// Download the image and add to deferred list
|
||||
val deferred = async(Dispatchers.IO) {
|
||||
var bitmap: Bitmap? = null
|
||||
var retryCount = 0
|
||||
|
||||
while (bitmap == null && retryCount < retries) {
|
||||
bitmap = imageData[index].fetchAndProcessImage(
|
||||
imageData[index].page,
|
||||
imageData[index].source,
|
||||
this@MangaDownloaderService
|
||||
)
|
||||
retryCount++
|
||||
}
|
||||
|
||||
// Cache the image if successful
|
||||
if (bitmap != null) {
|
||||
saveToDisk("$index.jpg", bitmap)
|
||||
}
|
||||
farthest++
|
||||
builder.setProgress(imageData.size, farthest + 1, false)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
|
||||
bitmap
|
||||
}
|
||||
|
||||
deferredList.add(deferred)
|
||||
}
|
||||
|
||||
// Wait for any remaining deferred to complete
|
||||
deferredList.awaitAll()
|
||||
|
||||
builder.setContentText("Download complete")
|
||||
.setProgress(0, 0, false)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
|
||||
saveMediaInfo()
|
||||
downloadsManager.addDownload(Download(title, chapter, Download.Type.MANGA))
|
||||
downloadsManager.exportDownloads(Download(title, chapter, Download.Type.MANGA))
|
||||
broadcastDownloadFinished(chapter)
|
||||
snackString("Download finished")
|
||||
stopSelf()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun saveToDisk(fileName: String, bitmap: Bitmap) {
|
||||
try {
|
||||
// Define the directory within the private external storage space
|
||||
val directory = File(
|
||||
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$title/$chapter"
|
||||
)
|
||||
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
// Create a file reference within that directory for your image
|
||||
val file = File(directory, fileName)
|
||||
|
||||
// Use a FileOutputStream to write the bitmap to the file
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
}
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("Exception while saving image: ${e.message}")
|
||||
Toast.makeText(this, "Exception while saving image: ${e.message}", Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveMediaInfo() {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$title/$chapter"
|
||||
)
|
||||
if (!directory.exists()) directory.mkdirs()
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.create()
|
||||
val mediaJson = gson.toJson(sourceMedia) //need a deep copy of sourceMedia
|
||||
val media = gson.fromJson(mediaJson, Media::class.java)
|
||||
if (media != null) {
|
||||
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
|
||||
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
|
||||
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
file.writeText(jsonString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun downloadImage(url: String, directory: File, name: String): String? = withContext(Dispatchers.IO) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
try {
|
||||
connection = URL(url).openConnection() as HttpURLConnection
|
||||
connection.connect()
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(this@MangaDownloaderService, "Exception while saving ${name}: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
null
|
||||
} finally {
|
||||
connection?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastDownloadStarted(chapterNumber: String) {
|
||||
val intent = Intent(ACTION_DOWNLOAD_STARTED).apply {
|
||||
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
private fun broadcastDownloadFinished(chapterNumber: String) {
|
||||
val intent = Intent(ACTION_DOWNLOAD_FINISHED).apply {
|
||||
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 1103
|
||||
}
|
||||
}
|
||||
|
||||
object ServiceDataSingleton {
|
||||
var imageData: List<ImageData> = listOf()
|
||||
var sourceMedia: Media? = null
|
||||
}
|
|
@ -28,6 +28,9 @@ import ani.dantotsu.parsers.Subtitle
|
|||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.*
|
||||
|
@ -118,6 +121,9 @@ object Helper {
|
|||
val database = StandaloneDatabaseProvider(context)
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
val okHttpClient = networkHelper.client
|
||||
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
defaultHeaders.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
|
|
|
@ -24,7 +24,7 @@ class MyDownloadService : DownloadService(1, 1, "download_service", R.string.dow
|
|||
override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification =
|
||||
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
||||
this,
|
||||
R.drawable.monochrome,
|
||||
R.drawable.mono,
|
||||
null,
|
||||
null,
|
||||
downloads,
|
||||
|
|
|
@ -22,7 +22,7 @@ data class Media(
|
|||
val userPreferredName: String,
|
||||
|
||||
var cover: String? = null,
|
||||
val banner: String? = null,
|
||||
var banner: String? = null,
|
||||
var relation: String? = null,
|
||||
var popularity: Int? = null,
|
||||
|
||||
|
|
|
@ -3,12 +3,14 @@ package ani.dantotsu.media
|
|||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||
|
@ -30,6 +32,8 @@ import ani.dantotsu.snackString
|
|||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.AniyomiAdapter
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
|
@ -44,6 +48,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class MediaDetailsViewModel : ViewModel() {
|
||||
val scrolledToTop = MutableLiveData(true)
|
||||
|
@ -258,7 +263,28 @@ class MediaDetailsViewModel : ViewModel() {
|
|||
|
||||
private val mangaChapter = MutableLiveData<MangaChapter?>(null)
|
||||
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
|
||||
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean {
|
||||
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, series: String, post: Boolean = true): Boolean {
|
||||
//check if the chapter has been downloaded already
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
if(downloadsManager.mangaDownloads.contains(Download(series, chapter.title!!, Download.Type.MANGA))) {
|
||||
val download = downloadsManager.mangaDownloads.find { it.title == series && it.chapter == chapter.title!! } ?: return false
|
||||
//look in the downloads folder for the chapter and add all the numerically named images to the chapter
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$series/${chapter.title!!}"
|
||||
)
|
||||
val images = mutableListOf<MangaImage>()
|
||||
directory.listFiles()?.forEach {
|
||||
if (it.nameWithoutExtension.toIntOrNull() != null) {
|
||||
images.add(MangaImage(FileUrl(it.absolutePath), false))
|
||||
}
|
||||
}
|
||||
//sort the images by name
|
||||
images.sortBy { it.url.url }
|
||||
chapter.addImages(images)
|
||||
if (post) mangaChapter.postValue(chapter)
|
||||
return true
|
||||
}
|
||||
return tryWithSuspend(true) {
|
||||
chapter.addImages(
|
||||
mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false
|
||||
|
|
|
@ -10,6 +10,9 @@ import android.os.Build
|
|||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.LruCache
|
||||
import android.widget.Toast
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.snackString
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -26,22 +29,23 @@ data class ImageData(
|
|||
try {
|
||||
// Fetch the image
|
||||
val response = httpSource.getImage(page)
|
||||
println("Response: ${response.code}")
|
||||
println("Response: ${response.message}")
|
||||
logger("Response: ${response.code}")
|
||||
logger("Response: ${response.message}")
|
||||
|
||||
// Convert the Response to an InputStream
|
||||
val inputStream = response.body?.byteStream()
|
||||
val inputStream = response.body.byteStream()
|
||||
|
||||
// Convert InputStream to Bitmap
|
||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
|
||||
inputStream?.close()
|
||||
saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
|
||||
inputStream.close()
|
||||
//saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
|
||||
|
||||
return@withContext bitmap
|
||||
} catch (e: Exception) {
|
||||
// Handle any exceptions
|
||||
println("An error occurred: ${e.message}")
|
||||
logger("An error occurred: ${e.message}")
|
||||
snackString("An error occurred: ${e.message}")
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +61,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
|
|||
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga")
|
||||
}
|
||||
|
||||
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||
val uri: Uri? = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
|
||||
uri?.let {
|
||||
contentResolver.openOutputStream(it)?.use { os ->
|
||||
|
@ -65,7 +69,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
|
|||
}
|
||||
}
|
||||
} else {
|
||||
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
|
||||
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Manga")
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ data class MangaChapter(
|
|||
var link: String,
|
||||
var title: String? = null,
|
||||
var description: String? = null,
|
||||
var sChapter: SChapter
|
||||
var sChapter: SChapter,
|
||||
) : Serializable {
|
||||
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ItemChapterListBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.media.Media
|
||||
|
@ -48,12 +49,71 @@ class MangaChapterAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
private val activeDownloads = mutableSetOf<String>()
|
||||
private val downloadedChapters = mutableSetOf<String>()
|
||||
|
||||
fun startDownload(chapterNumber: String) {
|
||||
activeDownloads.add(chapterNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
if (position != -1) {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopDownload(chapterNumber: String) {
|
||||
activeDownloads.remove(chapterNumber)
|
||||
downloadedChapters.add(chapterNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
if (position != -1) {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDownload(chapterNumber: String) {
|
||||
downloadedChapters.remove(chapterNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
if (position != -1) {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(chapterNumber: String) {
|
||||
if (activeDownloads.contains(chapterNumber)) {
|
||||
// Show spinner
|
||||
binding.itemDownload.setImageResource(R.drawable.spinner_icon_manga)
|
||||
} else if(downloadedChapters.contains(chapterNumber)) {
|
||||
// Show checkmark
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_check)
|
||||
} else {
|
||||
// Show download icon
|
||||
binding.itemDownload.setImageResource(R.drawable.ic_round_download_24)
|
||||
}
|
||||
}
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
||||
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
|
||||
}
|
||||
binding.itemDownload.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
|
||||
val chapterNumber = arr[bindingAdapterPosition].number
|
||||
if(activeDownloads.contains(chapterNumber)) {
|
||||
fragment.onMangaChapterStopDownloadClick(chapterNumber)
|
||||
return@setOnClickListener
|
||||
}else if(downloadedChapters.contains(chapterNumber)) {
|
||||
fragment.onMangaChapterRemoveDownloadClick(chapterNumber)
|
||||
return@setOnClickListener
|
||||
}else {
|
||||
fragment.onMangaChapterDownloadClick(chapterNumber)
|
||||
startDownload(chapterNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,6 +140,7 @@ class MangaChapterAdapter(
|
|||
is ChapterListViewHolder -> {
|
||||
val binding = holder.binding
|
||||
val ep = arr[position]
|
||||
holder.bind(ep.number)
|
||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||
binding.itemChapterNumber.text = ep.number
|
||||
if (!ep.title.isNullOrEmpty()) {
|
||||
|
|
|
@ -2,6 +2,11 @@ package ani.dantotsu.media.manga
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
|
@ -10,6 +15,7 @@ import android.view.ViewGroup
|
|||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
|
@ -20,10 +26,15 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.Download
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||
import ani.dantotsu.download.manga.ServiceDataSingleton
|
||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaParser
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
|
@ -41,8 +52,12 @@ import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
|||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
@ -62,6 +77,8 @@ open class MangaReadFragment : Fragment() {
|
|||
private lateinit var headerAdapter: MangaReadAdapter
|
||||
private lateinit var chapterAdapter: MangaChapterAdapter
|
||||
|
||||
val downloadManager = Injekt.get<DownloadsManager>()
|
||||
|
||||
var screenWidth = 0f
|
||||
private var progress = View.VISIBLE
|
||||
|
||||
|
@ -81,6 +98,11 @@ open class MangaReadFragment : Fragment() {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(ACTION_DOWNLOAD_STARTED)
|
||||
addAction(ACTION_DOWNLOAD_FINISHED)
|
||||
}
|
||||
requireContext().registerReceiver(downloadStatusReceiver, intentFilter)
|
||||
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
|
||||
screenWidth = resources.displayMetrics.widthPixels.dp
|
||||
|
||||
|
@ -132,6 +154,10 @@ open class MangaReadFragment : Fragment() {
|
|||
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
|
||||
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
|
||||
|
||||
for (download in downloadManager.mangaDownloads){
|
||||
chapterAdapter.stopDownload(download.chapter)
|
||||
}
|
||||
|
||||
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
|
@ -325,6 +351,57 @@ open class MangaReadFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
fun onMangaChapterDownloadClick(i: String) {
|
||||
model.continueMedia = false
|
||||
media.manga?.chapters?.get(i)?.let { chapter ->
|
||||
val parser = model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
|
||||
parser?.let {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// Fetch the image list and set it in the singleton
|
||||
ServiceDataSingleton.imageData = parser.imageList("", chapter.sChapter)
|
||||
|
||||
// Now that imageData is set, start the service
|
||||
ServiceDataSingleton.sourceMedia = media
|
||||
val intent = Intent(context, MangaDownloaderService::class.java).apply {
|
||||
putExtra("title", media.nameMAL)
|
||||
putExtra("chapter", chapter.title)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chapterAdapter.startDownload(i)
|
||||
ContextCompat.startForegroundService(requireContext(), intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun onMangaChapterRemoveDownloadClick(i: String){
|
||||
downloadManager.removeDownload(Download(media.nameMAL!!, i, Download.Type.MANGA))
|
||||
chapterAdapter.deleteDownload(i)
|
||||
}
|
||||
fun onMangaChapterStopDownloadClick(i: String) {
|
||||
val intent = Intent(requireContext(), MangaDownloaderService::class.java)
|
||||
requireContext().stopService(intent)
|
||||
downloadManager.removeDownload(Download(media.nameMAL!!, i, Download.Type.MANGA))
|
||||
chapterAdapter.deleteDownload(i)
|
||||
}
|
||||
private val downloadStatusReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
ACTION_DOWNLOAD_STARTED -> {
|
||||
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
|
||||
chapterNumber?.let { chapterAdapter.startDownload(it) }
|
||||
}
|
||||
ACTION_DOWNLOAD_FINISHED -> {
|
||||
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
|
||||
chapterNumber?.let { chapterAdapter.stopDownload(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun reload() {
|
||||
val selected = model.loadSelected(media)
|
||||
|
@ -353,6 +430,7 @@ open class MangaReadFragment : Fragment() {
|
|||
override fun onDestroy() {
|
||||
model.mangaReadSources?.flushText()
|
||||
super.onDestroy()
|
||||
requireContext().unregisterReceiver(downloadStatusReceiver)
|
||||
}
|
||||
|
||||
private var state: Parcelable? = null
|
||||
|
@ -366,4 +444,10 @@ open class MangaReadFragment : Fragment() {
|
|||
super.onPause()
|
||||
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
|
||||
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
|
||||
const val EXTRA_CHAPTER_NUMBER = "extra_chapter_number"
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.net.Uri
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
|
@ -29,6 +30,7 @@ import kotlinx.coroutines.withContext
|
|||
import ani.dantotsu.media.manga.MangaCache
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
abstract class BaseImageAdapter(
|
||||
val activity: MangaReaderActivity,
|
||||
|
@ -151,8 +153,9 @@ abstract class BaseImageAdapter(
|
|||
Glide.with(this@loadBitmap)
|
||||
.asBitmap()
|
||||
.let {
|
||||
if (link.url.startsWith("file://")) {
|
||||
it.load(link.url)
|
||||
val fileUri = Uri.fromFile(File(link.url)).toString()
|
||||
if (fileUri.startsWith("file://")) {
|
||||
it.load(fileUri)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
} else {
|
||||
|
|
|
@ -47,7 +47,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
|
|||
loaded = true
|
||||
binding.selectorAutoText.text = chp.title
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
if(model.loadMangaChapterImages(chp, m.selected!!)) {
|
||||
if(model.loadMangaChapterImages(chp, m.selected!!, m.nameMAL!!)) {
|
||||
val activity = currActivity()
|
||||
activity?.runOnUiThread {
|
||||
tryWith { dismiss() }
|
||||
|
|
|
@ -311,7 +311,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!) }
|
||||
scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!, media.nameMAL!!) }
|
||||
}
|
||||
|
||||
private val snapHelper = PagerSnapHelper()
|
||||
|
@ -700,6 +700,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
model.loadMangaChapterImages(
|
||||
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
|
||||
media.selected!!,
|
||||
media.nameMAL!!,
|
||||
false
|
||||
)
|
||||
loading = false
|
||||
|
|
|
@ -3,6 +3,7 @@ package ani.dantotsu.parsers
|
|||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
|
@ -10,11 +11,14 @@ import android.os.Build
|
|||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.FileUrl
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||
import ani.dantotsu.download.manga.ServiceDataSingleton
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
|
@ -65,18 +69,24 @@ class AniyomiAdapter {
|
|||
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
val extension: AnimeExtension.Installed
|
||||
var sourceLanguage = 0
|
||||
|
||||
init {
|
||||
this.extension = extension
|
||||
}
|
||||
|
||||
override val name = extension.name
|
||||
override val saveName = extension.name
|
||||
override val hostUrl = extension.sources.first().name
|
||||
override val isDubAvailableSeparately = false
|
||||
override val isNSFW = extension.isNsfw
|
||||
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> {
|
||||
val source = try{
|
||||
override suspend fun loadEpisodes(
|
||||
animeLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sAnime: SAnime
|
||||
): List<Episode> {
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
}
|
||||
|
@ -97,7 +107,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
var incrementingNumber = 1f
|
||||
sortedByStringNumber.map {
|
||||
if (it.episode_number == Float.MAX_VALUE) {
|
||||
it.episode_number = incrementingNumber++ // Update episode_number with the incrementing number
|
||||
it.episode_number =
|
||||
incrementingNumber++ // Update episode_number with the incrementing number
|
||||
}
|
||||
it
|
||||
}
|
||||
|
@ -117,10 +128,14 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
|
||||
}
|
||||
|
||||
override suspend fun loadVideoServers(episodeLink: String, extra: Map<String, String>?, sEpisode: SEpisode): List<VideoServer> {
|
||||
val source = try{
|
||||
override suspend fun loadVideoServers(
|
||||
episodeLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sEpisode: SEpisode
|
||||
): List<VideoServer> {
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? AnimeCatalogueSource ?: return emptyList()
|
||||
|
@ -140,9 +155,9 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
}
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val source = try{
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? AnimeCatalogueSource ?: return emptyList()
|
||||
|
@ -152,7 +167,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
} catch (e: CloudflareBypassException) {
|
||||
logger("Exception in search: $e")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
emptyList()
|
||||
} catch (e: Exception) {
|
||||
|
@ -186,9 +202,9 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
sEpisode.episode_number
|
||||
}
|
||||
return Episode(
|
||||
if(episodeNumberInt.toInt() != -1){
|
||||
if (episodeNumberInt.toInt() != -1) {
|
||||
episodeNumberInt.toString()
|
||||
}else{
|
||||
} else {
|
||||
sEpisode.name
|
||||
},
|
||||
sEpisode.url,
|
||||
|
@ -215,18 +231,24 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
val mangaCache = Injekt.get<MangaCache>()
|
||||
val extension: MangaExtension.Installed
|
||||
var sourceLanguage = 0
|
||||
|
||||
init {
|
||||
this.extension = extension
|
||||
}
|
||||
|
||||
override val name = extension.name
|
||||
override val saveName = extension.name
|
||||
override val hostUrl = extension.sources.first().name
|
||||
override val isNSFW = extension.isNsfw
|
||||
|
||||
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> {
|
||||
val source = try{
|
||||
override suspend fun loadChapters(
|
||||
mangaLink: String,
|
||||
extra: Map<String, String>?,
|
||||
sManga: SManga
|
||||
): List<MangaChapter> {
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? HttpSource ?: return emptyList()
|
||||
|
@ -247,37 +269,79 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
|
||||
|
||||
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
||||
val source = try{
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? HttpSource ?: return emptyList()
|
||||
|
||||
return coroutineScope {
|
||||
var imageDataList: List<ImageData> = listOf()
|
||||
val ret = coroutineScope {
|
||||
try {
|
||||
println("source.name " + source.name)
|
||||
println("source.name " + source.name)
|
||||
val res = source.getPageList(sChapter)
|
||||
val reIndexedPages = res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
||||
val reIndexedPages =
|
||||
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
||||
|
||||
val deferreds = reIndexedPages.map { page ->
|
||||
async(Dispatchers.IO) {
|
||||
mangaCache.put(page.imageUrl ?: "", ImageData(page, source))
|
||||
imageDataList += ImageData(page, source)
|
||||
logger("put page: ${page.imageUrl}")
|
||||
pageToMangaImage(page)
|
||||
}
|
||||
}
|
||||
|
||||
deferreds.awaitAll()
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger("loadImages Exception: $e")
|
||||
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? {
|
||||
suspend fun imageList(chapterLink: String, sChapter: SChapter): List<ImageData>{
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? HttpSource ?: return emptyList()
|
||||
var imageDataList: List<ImageData> = listOf()
|
||||
coroutineScope {
|
||||
try {
|
||||
println("source.name " + source.name)
|
||||
val res = source.getPageList(sChapter)
|
||||
val reIndexedPages =
|
||||
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
||||
|
||||
val deferreds = reIndexedPages.map { page ->
|
||||
async(Dispatchers.IO) {
|
||||
imageDataList += ImageData(page, source)
|
||||
}
|
||||
}
|
||||
|
||||
deferreds.awaitAll()
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger("loadImages Exception: $e")
|
||||
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
return imageDataList
|
||||
}
|
||||
|
||||
suspend fun fetchAndProcessImage(
|
||||
page: Page,
|
||||
httpSource: HttpSource,
|
||||
context: Context
|
||||
): Bitmap? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Fetch the image
|
||||
|
@ -310,7 +374,6 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
}
|
||||
|
||||
|
||||
|
||||
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
|
@ -325,7 +388,13 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
|
||||
withContext(Dispatchers.IO) {
|
||||
// Save the Bitmap using MediaStore API
|
||||
saveImage(bitmap, contentResolver, "image_${System.currentTimeMillis()}.jpg", Bitmap.CompressFormat.JPEG, 100)
|
||||
saveImage(
|
||||
bitmap,
|
||||
contentResolver,
|
||||
"image_${System.currentTimeMillis()}.jpg",
|
||||
Bitmap.CompressFormat.JPEG,
|
||||
100
|
||||
)
|
||||
}
|
||||
|
||||
inputStream?.close()
|
||||
|
@ -336,16 +405,28 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
}
|
||||
}
|
||||
|
||||
fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) {
|
||||
fun saveImage(
|
||||
bitmap: Bitmap,
|
||||
contentResolver: ContentResolver,
|
||||
filename: String,
|
||||
format: Bitmap.CompressFormat,
|
||||
quality: Int
|
||||
) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Anime")
|
||||
put(
|
||||
MediaStore.MediaColumns.RELATIVE_PATH,
|
||||
"${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Anime"
|
||||
)
|
||||
}
|
||||
|
||||
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||
val uri: Uri? = contentResolver.insert(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
contentValues
|
||||
)
|
||||
|
||||
uri?.let {
|
||||
contentResolver.openOutputStream(it)?.use { os ->
|
||||
|
@ -353,7 +434,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
|
||||
val directory =
|
||||
File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
@ -370,11 +452,10 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
}
|
||||
|
||||
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val source = try{
|
||||
val source = try {
|
||||
extension.sources[sourceLanguage]
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception) {
|
||||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? HttpSource ?: return emptyList()
|
||||
|
@ -386,7 +467,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
} catch (e: CloudflareBypassException) {
|
||||
logger("Exception in search: $e")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
emptyList()
|
||||
} catch (e: Exception) {
|
||||
|
@ -460,7 +542,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
//if (parsedChapterTitle.first != null || parsedChapterTitle.second != null) {
|
||||
// parsedChapterTitle.third
|
||||
//} else {
|
||||
sChapter.name,
|
||||
sChapter.name,
|
||||
//},
|
||||
null,
|
||||
sChapter
|
||||
|
@ -468,8 +550,10 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
}
|
||||
|
||||
fun parseChapterTitle(title: String): Triple<String?, String?, String> {
|
||||
val volumePattern = Pattern.compile("(?:vol\\.?|v|volume\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
|
||||
val chapterPattern = Pattern.compile("(?:ch\\.?|chapter\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
|
||||
val volumePattern =
|
||||
Pattern.compile("(?:vol\\.?|v|volume\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
|
||||
val chapterPattern =
|
||||
Pattern.compile("(?:ch\\.?|chapter\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
|
||||
|
||||
val volumeMatcher = volumePattern.matcher(title)
|
||||
val chapterMatcher = chapterPattern.matcher(title)
|
||||
|
@ -479,10 +563,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
|
||||
var remainingTitle = title
|
||||
if (volumeNumber != null) {
|
||||
remainingTitle = volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
||||
remainingTitle =
|
||||
volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
||||
}
|
||||
if (chapterNumber != null) {
|
||||
remainingTitle = chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
||||
remainingTitle =
|
||||
chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
||||
}
|
||||
|
||||
return Triple(volumeNumber, chapterNumber, remainingTitle.trim())
|
||||
|
@ -505,7 +591,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video) : ani.dantotsu.parsers.Video {
|
||||
private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video): ani.dantotsu.parsers.Video {
|
||||
// Find the number value from the .quality string
|
||||
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
|
||||
|
||||
|
@ -537,7 +623,8 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|||
logger("Unknown video format: $videoUrl")
|
||||
throw Exception("Unknown video format")
|
||||
}
|
||||
val headersMap: Map<String, String> = aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf()
|
||||
val headersMap: Map<String, String> =
|
||||
aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf()
|
||||
|
||||
|
||||
return ani.dantotsu.parsers.Video(
|
||||
|
@ -550,7 +637,11 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|||
|
||||
private fun getVideoType(fileName: String): VideoType? {
|
||||
return when {
|
||||
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER
|
||||
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(
|
||||
".mkv",
|
||||
ignoreCase = true
|
||||
) -> VideoType.CONTAINER
|
||||
|
||||
fileName.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8
|
||||
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
|
||||
else -> VideoType.CONTAINER
|
||||
|
@ -563,7 +654,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|||
runBlocking {
|
||||
type = findSubtitleType(track.url)
|
||||
}
|
||||
return Subtitle(track.lang, track.url, type?: SubtitleType.SRT)
|
||||
return Subtitle(track.lang, track.url, type ?: SubtitleType.SRT)
|
||||
}
|
||||
|
||||
private fun findSubtitleType(url: String): SubtitleType? {
|
||||
|
|
|
@ -81,8 +81,8 @@ data class MangaImage(
|
|||
|
||||
val useTransformation: Boolean = false,
|
||||
|
||||
val page: Page
|
||||
val page: Page? = null,
|
||||
) : Serializable{
|
||||
constructor(url: String,useTransformation: Boolean=false, page: Page)
|
||||
constructor(url: String,useTransformation: Boolean=false, page: Page? = null)
|
||||
: this(FileUrl(url),useTransformation, page)
|
||||
}
|
||||
|
|
|
@ -174,7 +174,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
|||
true
|
||||
}
|
||||
|
||||
val exDns = listOf("None", "Cloudflare", "Google", "AdGuard", "Quad9", "AliDNS", "DNSPod", "360", "Quad101", "Mullvad", "Controld", "Njalla", "Shecan")
|
||||
val exDns = listOf("None", "Cloudflare", "Google", "AdGuard", "Quad9", "AliDNS", "DNSPod", "360", "Quad101", "Mullvad", "Controld", "Njalla", "Shecan", "Libre")
|
||||
binding.settingsExtensionDns.setText(exDns[networkPreferences.dohProvider().get()], false)
|
||||
binding.settingsExtensionDns.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, exDns))
|
||||
binding.settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->
|
||||
|
|
|
@ -21,6 +21,7 @@ const val PREF_DOH_MULLVAD = 9
|
|||
const val PREF_DOH_CONTROLD = 10
|
||||
const val PREF_DOH_NJALLA = 11
|
||||
const val PREF_DOH_SHECAN = 12
|
||||
const val PREF_DOH_LIBREDNS = 13
|
||||
|
||||
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
|
@ -184,3 +185,13 @@ fun OkHttpClient.Builder.dohShecan() = dns(
|
|||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.dohLibreDNS() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://doh.libredns.gr/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("116.202.176.26"), // IPv4 address for LibreDNS
|
||||
InetAddress.getByName("192.71.166.92") // Fallback IPv4 address
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
|
|
@ -62,6 +62,7 @@ class NetworkHelper(
|
|||
PREF_DOH_CONTROLD -> builder.dohControlD()
|
||||
PREF_DOH_NJALLA -> builder.dohNajalla()
|
||||
PREF_DOH_SHECAN -> builder.dohShecan()
|
||||
PREF_DOH_LIBREDNS -> builder.dohLibreDNS()
|
||||
}
|
||||
|
||||
return builder
|
||||
|
|
9
app/src/main/res/drawable/ic_check.xml
Normal file
9
app/src/main/res/drawable/ic_check.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M382,720 L154,492l57,-57 171,171 367,-367 57,57 -424,424Z"/>
|
||||
</vector>
|
|
@ -70,6 +70,15 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemDownload"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:srcCompat="@drawable/ic_round_download_24"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<View
|
||||
android:id="@+id/itemEpisodeViewedCover"
|
||||
android:layout_width="match_parent"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue