From 20acd71b1a578678df702a5ce66f2b2e567855b0 Mon Sep 17 00:00:00 2001 From: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> Date: Fri, 3 Nov 2023 01:15:32 -0500 Subject: [PATCH] parent acb022569939d7aba7558d7fb91d7f27c8662a80 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 --- README.md | 4 +- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 2 + .../aniyomi/anime/custom/InjektModules.kt | 3 + .../ani/dantotsu/download/DownloadsManager.kt | 115 ++++++++ .../download/manga/MangaDownloaderService.kt | 273 ++++++++++++++++++ .../ani/dantotsu/download/video/Helper.kt | 6 + .../download/video/MyDownloadService.kt | 2 +- app/src/main/java/ani/dantotsu/media/Media.kt | 2 +- .../dantotsu/media/MediaDetailsViewModel.kt | 28 +- .../ani/dantotsu/media/manga/MangaCache.kt | 20 +- .../ani/dantotsu/media/manga/MangaChapter.kt | 2 +- .../media/manga/MangaChapterAdapter.kt | 61 ++++ .../dantotsu/media/manga/MangaReadFragment.kt | 84 ++++++ .../manga/mangareader/BaseImageAdapter.kt | 7 +- .../manga/mangareader/ChapterLoaderDialog.kt | 2 +- .../manga/mangareader/MangaReaderActivity.kt | 3 +- .../ani/dantotsu/parsers/AniyomiAdapter.kt | 175 ++++++++--- .../java/ani/dantotsu/parsers/MangaParser.kt | 4 +- .../ani/dantotsu/settings/SettingsActivity.kt | 2 +- .../kanade/tachiyomi/network/DohProviders.kt | 11 + .../kanade/tachiyomi/network/NetworkHelper.kt | 1 + app/src/main/res/drawable/ic_check.xml | 9 + app/src/main/res/layout/item_chapter_list.xml | 9 + 24 files changed, 763 insertions(+), 64 deletions(-) create mode 100644 app/src/main/java/ani/dantotsu/download/DownloadsManager.kt create mode 100644 app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt create mode 100644 app/src/main/res/drawable/ic_check.xml diff --git a/README.md b/README.md index 411408e7..a72536d1 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ Dantotsu is crafted from the ashes of Saikou and is based on simplistic yet stat -### 🌟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. diff --git a/app/build.gradle b/app/build.gradle index 1778b394..9a3930aa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,7 +62,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:2.8.1" implementation "org.jetbrains.kotlin:kotlin-reflect:$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 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' implementation 'androidx.preference:preference:1.2.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7201388e..8f3636aa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -267,6 +267,8 @@ + + \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt index 89280bae..ef2af1e9 100644 --- a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt +++ b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt @@ -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) } diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt new file mode 100644 index 00000000..a9981088 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt @@ -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 + get() = downloadsList.filter { it.type == Download.Type.MANGA } + val animeDownloads: List + 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 { + val jsonString = prefs.getString("downloads_key", null) + return if (jsonString != null) { + val type = object : TypeToken>() {}.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 + } +} diff --git a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt new file mode 100644 index 00000000..ce910675 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt @@ -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 = listOf() + private var sourceMedia: Media? = null + private lateinit var notificationManager: NotificationManagerCompat + private lateinit var builder: NotificationCompat.Builder + private val downloadsManager: DownloadsManager = Injekt.get() + + 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>() + + // 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 { + 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 = listOf() + var sourceMedia: Media? = null +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/video/Helper.kt b/app/src/main/java/ani/dantotsu/download/video/Helper.kt index eed4fa0e..15cbda59 100644 --- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt +++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt @@ -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() + val okHttpClient = networkHelper.client val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() defaultHeaders.forEach { dataSource.setRequestProperty(it.key, it.value) diff --git a/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt b/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt index 7398c008..18daad3a 100644 --- a/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt +++ b/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt @@ -24,7 +24,7 @@ class MyDownloadService : DownloadService(1, 1, "download_service", R.string.dow override fun getForegroundNotification(downloads: MutableList, notMetRequirements: Int): Notification = DownloadNotificationHelper(this, "download_service").buildProgressNotification( this, - R.drawable.monochrome, + R.drawable.mono, null, null, downloads, diff --git a/app/src/main/java/ani/dantotsu/media/Media.kt b/app/src/main/java/ani/dantotsu/media/Media.kt index 1e5ac178..872baf50 100644 --- a/app/src/main/java/ani/dantotsu/media/Media.kt +++ b/app/src/main/java/ani/dantotsu/media/Media.kt @@ -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, diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt index 203859a8..b730c87c 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt @@ -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(null) fun getMangaChapter(): LiveData = 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() + 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() + 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 diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt index ff6349c7..16fb83b9 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt @@ -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() } diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt index 16e51797..d0637c89 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt @@ -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) diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt index 36ad53bb..d399c89c 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt @@ -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() + private val downloadedChapters = mutableSetOf() + + 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()) { diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt index 61af0bf5..28b1ebbd 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -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() + 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" + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt index d3c63e2e..a23dc970 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt @@ -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 { diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt index e7041f0e..342203cd 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt @@ -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() } diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt index 94616d69..514db964 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt @@ -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 diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index b1f9cf33..41288e98 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt @@ -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?, sAnime: SAnime): List { - val source = try{ + override suspend fun loadEpisodes( + animeLink: String, + extra: Map?, + sAnime: SAnime + ): List { + 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?, sEpisode: SEpisode): List { - val source = try{ + override suspend fun loadVideoServers( + episodeLink: String, + extra: Map?, + sEpisode: SEpisode + ): List { + 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 { - 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() 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?, sManga: SManga): List { - val source = try{ + override suspend fun loadChapters( + mangaLink: String, + extra: Map?, + sManga: SManga + ): List { + 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 { - 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 = 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{ + val source = try { + extension.sources[sourceLanguage] + } catch (e: Exception) { + sourceLanguage = 0 + extension.sources[sourceLanguage] + } as? HttpSource ?: return emptyList() + var imageDataList: List = 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 { - 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 { - 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 = aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf() + val headersMap: Map = + 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? { diff --git a/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt index e555df82..0500cc32 100644 --- a/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt @@ -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) } diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt index d67a16ac..9975baf5 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt @@ -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, _ -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/DohProviders.kt b/app/src/main/java/eu/kanade/tachiyomi/network/DohProviders.kt index 95f448f1..5d2c65e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/DohProviders.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/DohProviders.kt @@ -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() +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index fdd7bd69..b0efde9f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -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 diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 00000000..f97e17d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/item_chapter_list.xml b/app/src/main/res/layout/item_chapter_list.xml index b3cae860..dfa0e103 100644 --- a/app/src/main/res/layout/item_chapter_list.xml +++ b/app/src/main/res/layout/item_chapter_list.xml @@ -70,6 +70,15 @@ + +