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
|
@ -15,9 +15,9 @@ Dantotsu is crafted from the ashes of Saikou and is based on simplistic yet stat
|
||||||
|
|
||||||
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
|
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
|
||||||
|
|
||||||
### 🌟STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
|
### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
|
||||||
|
|
||||||
> **WARNING**
|
> **WARNING ⚠️**
|
||||||
>
|
>
|
||||||
> Please do not attempt to upload Dantotsu or any of its forks on Playstore or any other Android app stores on the internet. Doing so may infringe their terms and conditions and result in legal action or immediate take-down of the app.
|
> Please do not attempt to upload Dantotsu or any of its forks on Playstore or any other Android app stores on the internet. Doing so may infringe their terms and conditions and result in legal action or immediate take-down of the app.
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ dependencies {
|
||||||
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
implementation 'com.google.code.gson:gson:2.8.9'
|
||||||
implementation 'com.github.Blatzar:NiceHttp:0.4.3'
|
implementation 'com.github.Blatzar:NiceHttp:0.4.3'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
||||||
implementation 'androidx.preference:preference:1.2.1'
|
implementation 'androidx.preference:preference:1.2.1'
|
||||||
|
|
|
@ -267,6 +267,8 @@
|
||||||
|
|
||||||
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service android:name=".download.manga.MangaDownloaderService" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -23,11 +23,14 @@ import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addSingleton
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
import uy.kohesive.injekt.api.addSingletonFactory
|
import uy.kohesive.injekt.api.addSingletonFactory
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
|
||||||
class AppModule(val app: Application) : InjektModule {
|
class AppModule(val app: Application) : InjektModule {
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
addSingleton(app)
|
addSingleton(app)
|
||||||
|
|
||||||
|
addSingletonFactory { DownloadsManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { NetworkHelper(app, get()) }
|
addSingletonFactory { NetworkHelper(app, get()) }
|
||||||
|
|
||||||
addSingletonFactory { AnimeExtensionManager(app) }
|
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.SubtitleType
|
||||||
import ani.dantotsu.parsers.Video
|
import ani.dantotsu.parsers.Video
|
||||||
import ani.dantotsu.parsers.VideoType
|
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.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.*
|
||||||
|
@ -118,6 +121,9 @@ object Helper {
|
||||||
val database = StandaloneDatabaseProvider(context)
|
val database = StandaloneDatabaseProvider(context)
|
||||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||||
val dataSourceFactory = DataSource.Factory {
|
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()
|
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||||
defaultHeaders.forEach {
|
defaultHeaders.forEach {
|
||||||
dataSource.setRequestProperty(it.key, it.value)
|
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 =
|
override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification =
|
||||||
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
||||||
this,
|
this,
|
||||||
R.drawable.monochrome,
|
R.drawable.mono,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
downloads,
|
downloads,
|
||||||
|
|
|
@ -22,7 +22,7 @@ data class Media(
|
||||||
val userPreferredName: String,
|
val userPreferredName: String,
|
||||||
|
|
||||||
var cover: String? = null,
|
var cover: String? = null,
|
||||||
val banner: String? = null,
|
var banner: String? = null,
|
||||||
var relation: String? = null,
|
var relation: String? = null,
|
||||||
var popularity: Int? = null,
|
var popularity: Int? = null,
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,14 @@ package ani.dantotsu.media
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.os.Environment
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import ani.dantotsu.FileUrl
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.media.anime.Episode
|
import ani.dantotsu.media.anime.Episode
|
||||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||||
|
@ -30,6 +32,8 @@ import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.tryWithSuspend
|
import ani.dantotsu.tryWithSuspend
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.download.Download
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.AniyomiAdapter
|
import ani.dantotsu.parsers.AniyomiAdapter
|
||||||
import ani.dantotsu.parsers.DynamicMangaParser
|
import ani.dantotsu.parsers.DynamicMangaParser
|
||||||
|
@ -44,6 +48,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class MediaDetailsViewModel : ViewModel() {
|
class MediaDetailsViewModel : ViewModel() {
|
||||||
val scrolledToTop = MutableLiveData(true)
|
val scrolledToTop = MutableLiveData(true)
|
||||||
|
@ -258,7 +263,28 @@ class MediaDetailsViewModel : ViewModel() {
|
||||||
|
|
||||||
private val mangaChapter = MutableLiveData<MangaChapter?>(null)
|
private val mangaChapter = MutableLiveData<MangaChapter?>(null)
|
||||||
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
|
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) {
|
return tryWithSuspend(true) {
|
||||||
chapter.addImages(
|
chapter.addImages(
|
||||||
mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false
|
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.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.LruCache
|
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.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -26,22 +29,23 @@ data class ImageData(
|
||||||
try {
|
try {
|
||||||
// Fetch the image
|
// Fetch the image
|
||||||
val response = httpSource.getImage(page)
|
val response = httpSource.getImage(page)
|
||||||
println("Response: ${response.code}")
|
logger("Response: ${response.code}")
|
||||||
println("Response: ${response.message}")
|
logger("Response: ${response.message}")
|
||||||
|
|
||||||
// Convert the Response to an InputStream
|
// Convert the Response to an InputStream
|
||||||
val inputStream = response.body?.byteStream()
|
val inputStream = response.body.byteStream()
|
||||||
|
|
||||||
// Convert InputStream to Bitmap
|
// Convert InputStream to Bitmap
|
||||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
|
||||||
inputStream?.close()
|
inputStream.close()
|
||||||
saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
|
//saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
|
||||||
|
|
||||||
return@withContext bitmap
|
return@withContext bitmap
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Handle any exceptions
|
// Handle any exceptions
|
||||||
println("An error occurred: ${e.message}")
|
logger("An error occurred: ${e.message}")
|
||||||
|
snackString("An error occurred: ${e.message}")
|
||||||
return@withContext null
|
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")
|
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 {
|
uri?.let {
|
||||||
contentResolver.openOutputStream(it)?.use { os ->
|
contentResolver.openOutputStream(it)?.use { os ->
|
||||||
|
@ -65,7 +69,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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()) {
|
if (!directory.exists()) {
|
||||||
directory.mkdirs()
|
directory.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ data class MangaChapter(
|
||||||
var link: String,
|
var link: String,
|
||||||
var title: String? = null,
|
var title: String? = null,
|
||||||
var description: String? = null,
|
var description: String? = null,
|
||||||
var sChapter: SChapter
|
var sChapter: SChapter,
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter)
|
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.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.databinding.ItemChapterListBinding
|
import ani.dantotsu.databinding.ItemChapterListBinding
|
||||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||||
import ani.dantotsu.media.Media
|
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) {
|
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 {
|
init {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
||||||
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
|
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 -> {
|
is ChapterListViewHolder -> {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
val ep = arr[position]
|
val ep = arr[position]
|
||||||
|
holder.bind(ep.number)
|
||||||
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
|
||||||
binding.itemChapterNumber.text = ep.number
|
binding.itemChapterNumber.text = ep.number
|
||||||
if (!ep.title.isNullOrEmpty()) {
|
if (!ep.title.isNullOrEmpty()) {
|
||||||
|
|
|
@ -2,6 +2,11 @@ package ani.dantotsu.media.manga
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.AlertDialog
|
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.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -10,6 +15,7 @@ import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.math.MathUtils.clamp
|
import androidx.core.math.MathUtils.clamp
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -20,10 +26,15 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.*
|
||||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
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.manga.mangareader.ChapterLoaderDialog
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
|
import ani.dantotsu.parsers.DynamicMangaParser
|
||||||
import ani.dantotsu.parsers.HMangaSources
|
import ani.dantotsu.parsers.HMangaSources
|
||||||
import ani.dantotsu.parsers.MangaParser
|
import ani.dantotsu.parsers.MangaParser
|
||||||
import ani.dantotsu.parsers.MangaSources
|
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.anime.model.AnimeExtension
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
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.ceil
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
@ -62,6 +77,8 @@ open class MangaReadFragment : Fragment() {
|
||||||
private lateinit var headerAdapter: MangaReadAdapter
|
private lateinit var headerAdapter: MangaReadAdapter
|
||||||
private lateinit var chapterAdapter: MangaChapterAdapter
|
private lateinit var chapterAdapter: MangaChapterAdapter
|
||||||
|
|
||||||
|
val downloadManager = Injekt.get<DownloadsManager>()
|
||||||
|
|
||||||
var screenWidth = 0f
|
var screenWidth = 0f
|
||||||
private var progress = View.VISIBLE
|
private var progress = View.VISIBLE
|
||||||
|
|
||||||
|
@ -81,6 +98,11 @@ open class MangaReadFragment : Fragment() {
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
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)
|
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
|
||||||
screenWidth = resources.displayMetrics.widthPixels.dp
|
screenWidth = resources.displayMetrics.widthPixels.dp
|
||||||
|
|
||||||
|
@ -132,6 +154,10 @@ open class MangaReadFragment : Fragment() {
|
||||||
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
|
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
|
||||||
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
|
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
|
||||||
|
|
||||||
|
for (download in downloadManager.mangaDownloads){
|
||||||
|
chapterAdapter.stopDownload(download.chapter)
|
||||||
|
}
|
||||||
|
|
||||||
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
|
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
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")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun reload() {
|
private fun reload() {
|
||||||
val selected = model.loadSelected(media)
|
val selected = model.loadSelected(media)
|
||||||
|
@ -353,6 +430,7 @@ open class MangaReadFragment : Fragment() {
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
model.mangaReadSources?.flushText()
|
model.mangaReadSources?.flushText()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
requireContext().unregisterReceiver(downloadStatusReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var state: Parcelable? = null
|
private var state: Parcelable? = null
|
||||||
|
@ -366,4 +444,10 @@ open class MangaReadFragment : Fragment() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
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.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.net.Uri
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -29,6 +30,7 @@ import kotlinx.coroutines.withContext
|
||||||
import ani.dantotsu.media.manga.MangaCache
|
import ani.dantotsu.media.manga.MangaCache
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
abstract class BaseImageAdapter(
|
abstract class BaseImageAdapter(
|
||||||
val activity: MangaReaderActivity,
|
val activity: MangaReaderActivity,
|
||||||
|
@ -151,8 +153,9 @@ abstract class BaseImageAdapter(
|
||||||
Glide.with(this@loadBitmap)
|
Glide.with(this@loadBitmap)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.let {
|
.let {
|
||||||
if (link.url.startsWith("file://")) {
|
val fileUri = Uri.fromFile(File(link.url)).toString()
|
||||||
it.load(link.url)
|
if (fileUri.startsWith("file://")) {
|
||||||
|
it.load(fileUri)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -47,7 +47,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
|
||||||
loaded = true
|
loaded = true
|
||||||
binding.selectorAutoText.text = chp.title
|
binding.selectorAutoText.text = chp.title
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
if(model.loadMangaChapterImages(chp, m.selected!!)) {
|
if(model.loadMangaChapterImages(chp, m.selected!!, m.nameMAL!!)) {
|
||||||
val activity = currActivity()
|
val activity = currActivity()
|
||||||
activity?.runOnUiThread {
|
activity?.runOnUiThread {
|
||||||
tryWith { dismiss() }
|
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()
|
private val snapHelper = PagerSnapHelper()
|
||||||
|
@ -700,6 +700,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||||
model.loadMangaChapterImages(
|
model.loadMangaChapterImages(
|
||||||
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
|
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
|
||||||
media.selected!!,
|
media.selected!!,
|
||||||
|
media.nameMAL!!,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
loading = false
|
loading = false
|
||||||
|
|
|
@ -3,6 +3,7 @@ package ani.dantotsu.parsers
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -10,11 +11,14 @@ import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import ani.dantotsu.FileUrl
|
import ani.dantotsu.FileUrl
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
|
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
|
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||||
|
import ani.dantotsu.download.manga.ServiceDataSingleton
|
||||||
import ani.dantotsu.logger
|
import ani.dantotsu.logger
|
||||||
import ani.dantotsu.media.manga.ImageData
|
import ani.dantotsu.media.manga.ImageData
|
||||||
import ani.dantotsu.media.manga.MangaCache
|
import ani.dantotsu.media.manga.MangaCache
|
||||||
|
@ -65,15 +69,21 @@ class AniyomiAdapter {
|
||||||
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||||
val extension: AnimeExtension.Installed
|
val extension: AnimeExtension.Installed
|
||||||
var sourceLanguage = 0
|
var sourceLanguage = 0
|
||||||
|
|
||||||
init {
|
init {
|
||||||
this.extension = extension
|
this.extension = extension
|
||||||
}
|
}
|
||||||
|
|
||||||
override val name = extension.name
|
override val name = extension.name
|
||||||
override val saveName = extension.name
|
override val saveName = extension.name
|
||||||
override val hostUrl = extension.sources.first().name
|
override val hostUrl = extension.sources.first().name
|
||||||
override val isDubAvailableSeparately = false
|
override val isDubAvailableSeparately = false
|
||||||
override val isNSFW = extension.isNsfw
|
override val isNSFW = extension.isNsfw
|
||||||
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> {
|
override suspend fun loadEpisodes(
|
||||||
|
animeLink: String,
|
||||||
|
extra: Map<String, String>?,
|
||||||
|
sAnime: SAnime
|
||||||
|
): List<Episode> {
|
||||||
val source = try {
|
val source = try {
|
||||||
extension.sources[sourceLanguage]
|
extension.sources[sourceLanguage]
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -97,7 +107,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||||
var incrementingNumber = 1f
|
var incrementingNumber = 1f
|
||||||
sortedByStringNumber.map {
|
sortedByStringNumber.map {
|
||||||
if (it.episode_number == Float.MAX_VALUE) {
|
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
|
it
|
||||||
}
|
}
|
||||||
|
@ -117,7 +128,11 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||||
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
|
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> {
|
override suspend fun loadVideoServers(
|
||||||
|
episodeLink: String,
|
||||||
|
extra: Map<String, String>?,
|
||||||
|
sEpisode: SEpisode
|
||||||
|
): List<VideoServer> {
|
||||||
val source = try {
|
val source = try {
|
||||||
extension.sources[sourceLanguage]
|
extension.sources[sourceLanguage]
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -152,7 +167,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||||
} catch (e: CloudflareBypassException) {
|
} catch (e: CloudflareBypassException) {
|
||||||
logger("Exception in search: $e")
|
logger("Exception in search: $e")
|
||||||
withContext(Dispatchers.Main) {
|
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()
|
emptyList()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -215,15 +231,21 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
val mangaCache = Injekt.get<MangaCache>()
|
val mangaCache = Injekt.get<MangaCache>()
|
||||||
val extension: MangaExtension.Installed
|
val extension: MangaExtension.Installed
|
||||||
var sourceLanguage = 0
|
var sourceLanguage = 0
|
||||||
|
|
||||||
init {
|
init {
|
||||||
this.extension = extension
|
this.extension = extension
|
||||||
}
|
}
|
||||||
|
|
||||||
override val name = extension.name
|
override val name = extension.name
|
||||||
override val saveName = extension.name
|
override val saveName = extension.name
|
||||||
override val hostUrl = extension.sources.first().name
|
override val hostUrl = extension.sources.first().name
|
||||||
override val isNSFW = extension.isNsfw
|
override val isNSFW = extension.isNsfw
|
||||||
|
|
||||||
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> {
|
override suspend fun loadChapters(
|
||||||
|
mangaLink: String,
|
||||||
|
extra: Map<String, String>?,
|
||||||
|
sManga: SManga
|
||||||
|
): List<MangaChapter> {
|
||||||
val source = try {
|
val source = try {
|
||||||
extension.sources[sourceLanguage]
|
extension.sources[sourceLanguage]
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -253,31 +275,73 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
sourceLanguage = 0
|
sourceLanguage = 0
|
||||||
extension.sources[sourceLanguage]
|
extension.sources[sourceLanguage]
|
||||||
} as? HttpSource ?: return emptyList()
|
} as? HttpSource ?: return emptyList()
|
||||||
|
var imageDataList: List<ImageData> = listOf()
|
||||||
return coroutineScope {
|
val ret = coroutineScope {
|
||||||
try {
|
try {
|
||||||
println("source.name " + source.name)
|
println("source.name " + source.name)
|
||||||
val res = source.getPageList(sChapter)
|
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 ->
|
val deferreds = reIndexedPages.map { page ->
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
mangaCache.put(page.imageUrl ?: "", ImageData(page, source))
|
mangaCache.put(page.imageUrl ?: "", ImageData(page, source))
|
||||||
|
imageDataList += ImageData(page, source)
|
||||||
logger("put page: ${page.imageUrl}")
|
logger("put page: ${page.imageUrl}")
|
||||||
pageToMangaImage(page)
|
pageToMangaImage(page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deferreds.awaitAll()
|
deferreds.awaitAll()
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger("loadImages Exception: $e")
|
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()
|
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) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
// Fetch the image
|
// Fetch the image
|
||||||
|
@ -310,7 +374,6 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
|
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
|
@ -325,7 +388,13 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// Save the Bitmap using MediaStore API
|
// 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()
|
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 {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val contentValues = ContentValues().apply {
|
val contentValues = ContentValues().apply {
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
|
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 {
|
uri?.let {
|
||||||
contentResolver.openOutputStream(it)?.use { os ->
|
contentResolver.openOutputStream(it)?.use { os ->
|
||||||
|
@ -353,7 +434,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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()) {
|
if (!directory.exists()) {
|
||||||
directory.mkdirs()
|
directory.mkdirs()
|
||||||
}
|
}
|
||||||
|
@ -370,7 +452,6 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override suspend fun search(query: String): List<ShowResponse> {
|
override suspend fun search(query: String): List<ShowResponse> {
|
||||||
val source = try {
|
val source = try {
|
||||||
extension.sources[sourceLanguage]
|
extension.sources[sourceLanguage]
|
||||||
|
@ -386,7 +467,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
} catch (e: CloudflareBypassException) {
|
} catch (e: CloudflareBypassException) {
|
||||||
logger("Exception in search: $e")
|
logger("Exception in search: $e")
|
||||||
withContext(Dispatchers.Main) {
|
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()
|
emptyList()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -468,8 +550,10 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseChapterTitle(title: String): Triple<String?, String?, String> {
|
fun parseChapterTitle(title: String): Triple<String?, String?, String> {
|
||||||
val volumePattern = Pattern.compile("(?:vol\\.?|v|volume\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
|
val volumePattern =
|
||||||
val chapterPattern = Pattern.compile("(?:ch\\.?|chapter\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
|
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 volumeMatcher = volumePattern.matcher(title)
|
||||||
val chapterMatcher = chapterPattern.matcher(title)
|
val chapterMatcher = chapterPattern.matcher(title)
|
||||||
|
@ -479,10 +563,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
|
|
||||||
var remainingTitle = title
|
var remainingTitle = title
|
||||||
if (volumeNumber != null) {
|
if (volumeNumber != null) {
|
||||||
remainingTitle = volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
remainingTitle =
|
||||||
|
volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
||||||
}
|
}
|
||||||
if (chapterNumber != null) {
|
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())
|
return Triple(volumeNumber, chapterNumber, remainingTitle.trim())
|
||||||
|
@ -537,7 +623,8 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||||
logger("Unknown video format: $videoUrl")
|
logger("Unknown video format: $videoUrl")
|
||||||
throw Exception("Unknown video format")
|
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(
|
return ani.dantotsu.parsers.Video(
|
||||||
|
@ -550,7 +637,11 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||||
|
|
||||||
private fun getVideoType(fileName: String): VideoType? {
|
private fun getVideoType(fileName: String): VideoType? {
|
||||||
return when {
|
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(".m3u8", ignoreCase = true) -> VideoType.M3U8
|
||||||
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
|
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
|
||||||
else -> VideoType.CONTAINER
|
else -> VideoType.CONTAINER
|
||||||
|
|
|
@ -81,8 +81,8 @@ data class MangaImage(
|
||||||
|
|
||||||
val useTransformation: Boolean = false,
|
val useTransformation: Boolean = false,
|
||||||
|
|
||||||
val page: Page
|
val page: Page? = null,
|
||||||
) : Serializable{
|
) : Serializable{
|
||||||
constructor(url: String,useTransformation: Boolean=false, page: Page)
|
constructor(url: String,useTransformation: Boolean=false, page: Page? = null)
|
||||||
: this(FileUrl(url),useTransformation, page)
|
: this(FileUrl(url),useTransformation, page)
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,7 +174,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
|
||||||
true
|
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.setText(exDns[networkPreferences.dohProvider().get()], false)
|
||||||
binding.settingsExtensionDns.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, exDns))
|
binding.settingsExtensionDns.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, exDns))
|
||||||
binding.settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->
|
binding.settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->
|
||||||
|
|
|
@ -21,6 +21,7 @@ const val PREF_DOH_MULLVAD = 9
|
||||||
const val PREF_DOH_CONTROLD = 10
|
const val PREF_DOH_CONTROLD = 10
|
||||||
const val PREF_DOH_NJALLA = 11
|
const val PREF_DOH_NJALLA = 11
|
||||||
const val PREF_DOH_SHECAN = 12
|
const val PREF_DOH_SHECAN = 12
|
||||||
|
const val PREF_DOH_LIBREDNS = 13
|
||||||
|
|
||||||
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
||||||
DnsOverHttps.Builder().client(build())
|
DnsOverHttps.Builder().client(build())
|
||||||
|
@ -184,3 +185,13 @@ fun OkHttpClient.Builder.dohShecan() = dns(
|
||||||
)
|
)
|
||||||
.build(),
|
.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_CONTROLD -> builder.dohControlD()
|
||||||
PREF_DOH_NJALLA -> builder.dohNajalla()
|
PREF_DOH_NJALLA -> builder.dohNajalla()
|
||||||
PREF_DOH_SHECAN -> builder.dohShecan()
|
PREF_DOH_SHECAN -> builder.dohShecan()
|
||||||
|
PREF_DOH_LIBREDNS -> builder.dohLibreDNS()
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder
|
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>
|
</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
|
<View
|
||||||
android:id="@+id/itemEpisodeViewedCover"
|
android:id="@+id/itemEpisodeViewedCover"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue