diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt index 89a07b0e..e3b19ae1 100644 --- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt +++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt @@ -48,6 +48,32 @@ class DownloadsManager(private val context: Context) { saveDownloads() } + fun removeMedia(title: String, type: Download.Type) { + val subDirectory = if (type == Download.Type.MANGA) { + "Manga" + } else if (type == Download.Type.ANIME) { + "Anime" + } else { + "Novel" + } + val directory = File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/$subDirectory/$title" + ) + 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.title == title } + saveDownloads() + } + fun queryDownload(download: Download): Boolean { return downloadsList.contains(download) } diff --git a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt index aa53a3f7..fce89d3e 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt @@ -20,6 +20,7 @@ import androidx.core.content.ContextCompat import ani.dantotsu.R import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadsManager +import ani.dantotsu.logger import ani.dantotsu.media.Media import ani.dantotsu.media.manga.ImageData import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED @@ -175,74 +176,90 @@ class MangaDownloaderService : Service() { } suspend fun download(task: DownloadTask) { - withContext(Dispatchers.Main) { - val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - ContextCompat.checkSelfPermission( - this@MangaDownloaderService, - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - } else { - true - } - - val deferredList = mutableListOf>() - builder.setContentText("Downloading ${task.title} - ${task.chapter}") - if (notifi) { - notificationManager.notify(NOTIFICATION_ID, builder.build()) - } - - // Loop through each ImageData object from the task - var farthest = 0 - for ((index, image) in task.imageData.withIndex()) { - // Limit the number of simultaneous downloads from the task - if (deferredList.size >= task.simultaneousDownloads) { - // Wait for all deferred to complete and clear the list - deferredList.awaitAll() - deferredList.clear() + try { + withContext(Dispatchers.Main) { + val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission( + this@MangaDownloaderService, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } else { + true } - // Download the image and add to deferred list - val deferred = async(Dispatchers.IO) { - var bitmap: Bitmap? = null - var retryCount = 0 + val deferredList = mutableListOf>() + builder.setContentText("Downloading ${task.title} - ${task.chapter}") + if (notifi) { + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } - while (bitmap == null && retryCount < task.retries) { - bitmap = image.fetchAndProcessImage( - image.page, - image.source, - this@MangaDownloaderService + // Loop through each ImageData object from the task + var farthest = 0 + for ((index, image) in task.imageData.withIndex()) { + // Limit the number of simultaneous downloads from the task + if (deferredList.size >= task.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 < task.retries) { + bitmap = image.fetchAndProcessImage( + image.page, + image.source, + this@MangaDownloaderService + ) + retryCount++ + } + + // Cache the image if successful + if (bitmap != null) { + saveToDisk("$index.jpg", bitmap, task.title, task.chapter) + } + farthest++ + builder.setProgress(task.imageData.size, farthest, false) + broadcastDownloadProgress( + task.chapter, + farthest * 100 / task.imageData.size ) - retryCount++ + if (notifi) { + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } + + bitmap } - // Cache the image if successful - if (bitmap != null) { - saveToDisk("$index.jpg", bitmap, task.title, task.chapter) - } - farthest++ - builder.setProgress(task.imageData.size, farthest, false) - broadcastDownloadProgress(task.chapter, farthest * 100 / task.imageData.size) - if (notifi) { - notificationManager.notify(NOTIFICATION_ID, builder.build()) - } - - bitmap + deferredList.add(deferred) } - deferredList.add(deferred) + // Wait for any remaining deferred to complete + deferredList.awaitAll() + + builder.setContentText("${task.title} - ${task.chapter} Download complete") + .setProgress(0, 0, false) + notificationManager.notify(NOTIFICATION_ID, builder.build()) + + saveMediaInfo(task) + downloadsManager.addDownload( + Download( + task.title, + task.chapter, + Download.Type.MANGA + ) + ) + broadcastDownloadFinished(task.chapter) + snackString("${task.title} - ${task.chapter} Download finished") } - - // Wait for any remaining deferred to complete - deferredList.awaitAll() - - builder.setContentText("${task.title} - ${task.chapter} Download complete") - .setProgress(0, 0, false) - notificationManager.notify(NOTIFICATION_ID, builder.build()) - - saveMediaInfo(task) - downloadsManager.addDownload(Download(task.title, task.chapter, Download.Type.MANGA)) - broadcastDownloadFinished(task.chapter) - snackString("${task.title} - ${task.chapter} Download finished") + } catch (e: Exception) { + logger("Exception while downloading file: ${e.message}") + snackString("Exception while downloading file: ${e.message}") + FirebaseCrashlytics.getInstance().recordException(e) + broadcastDownloadFailed(task.chapter) } } diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt index 7f304b11..b91bb55a 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt @@ -13,10 +13,12 @@ import ani.dantotsu.R class OfflineMangaAdapter( private val context: Context, - private val items: List + private var items: List, + private val searchListener: OfflineMangaSearchListener ) : BaseAdapter() { private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + private var originalItems: List = items override fun getCount(): Int { return items.size @@ -54,4 +56,22 @@ class OfflineMangaAdapter( } return view } + + fun onSearchQuery(query: String) { + // Implement the filtering logic here, for example: + items = if (query.isEmpty()) { + // Return the original list if the query is empty + originalItems + } else { + // Filter the list based on the query + originalItems.filter { it.title.contains(query, ignoreCase = true) } + } + notifyDataSetChanged() // Notify the adapter that the data set has changed + } + + fun setItems(items: List) { + this.items = items + this.originalItems = items + notifyDataSetChanged() + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt index 3b869ca0..9e237dae 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt @@ -7,11 +7,14 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Environment +import android.text.Editable +import android.text.TextWatcher import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.OvershootInterpolator +import android.widget.AutoCompleteTextView import android.widget.GridView import androidx.appcompat.app.AppCompatActivity import androidx.cardview.widget.CardView @@ -41,7 +44,7 @@ import java.io.File import kotlin.math.max import kotlin.math.min -class OfflineMangaFragment : Fragment() { +class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { private val downloadManager = Injekt.get() private var downloads: List = listOf() private lateinit var gridView: GridView @@ -79,15 +82,29 @@ class OfflineMangaFragment : Fragment() { materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt()) } + val searchView = view.findViewById(R.id.animeSearchBarText) + searchView.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + onSearchQuery(s.toString()) + } + }) + gridView = view.findViewById(R.id.gridView) getDownloads() - adapter = OfflineMangaAdapter(requireContext(), downloads) + adapter = OfflineMangaAdapter(requireContext(), downloads, this) gridView.adapter = adapter gridView.setOnItemClickListener { parent, view, position, id -> // Get the OfflineMangaModel that was clicked val item = adapter.getItem(position) as OfflineMangaModel val media = - downloadManager.mangaDownloads.filter { it.title == item.title }.firstOrNull() + downloadManager.mangaDownloads.firstOrNull { it.title == item.title } + ?: downloadManager.novelDownloads.firstOrNull { it.title == item.title } media?.let { startActivity( Intent(requireContext(), MediaDetailsActivity::class.java) @@ -99,9 +116,37 @@ class OfflineMangaFragment : Fragment() { } } + gridView.setOnItemLongClickListener { parent, view, position, id -> + // Get the OfflineMangaModel that was clicked + val item = adapter.getItem(position) as OfflineMangaModel + val type: Download.Type = if (downloadManager.mangaDownloads.any { it.title == item.title }) { + Download.Type.MANGA + } else { + Download.Type.NOVEL + } + // Alert dialog to confirm deletion + val builder = androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.DialogTheme) + builder.setTitle("Delete ${item.title}?") + builder.setMessage("Are you sure you want to delete ${item.title}?") + builder.setPositiveButton("Yes") { _, _ -> + downloadManager.removeMedia(item.title, type) + getDownloads() + adapter.setItems(downloads) + } + builder.setNegativeButton("No") { _, _ -> + // Do nothing + } + builder.show() + true + } + return view } + override fun onSearchQuery(query: String) { + adapter.onSearchQuery(query) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) var height = statusBarHeight @@ -164,24 +209,42 @@ class OfflineMangaFragment : Fragment() { } private fun getDownloads() { - val titles = downloadManager.mangaDownloads.map { it.title }.distinct() - val newDownloads = mutableListOf() - for (title in titles) { + downloads = listOf() + val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct() + val newMangaDownloads = mutableListOf() + for (title in mangaTitles) { val _downloads = downloadManager.mangaDownloads.filter { it.title == title } val download = _downloads.first() val offlineMangaModel = loadOfflineMangaModel(download) - newDownloads += offlineMangaModel + newMangaDownloads += offlineMangaModel } - downloads = newDownloads + downloads = newMangaDownloads + val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct() + val newNovelDownloads = mutableListOf() + for (title in novelTitles) { + val _downloads = downloadManager.novelDownloads.filter { it.title == title } + val download = _downloads.first() + val offlineMangaModel = loadOfflineMangaModel(download) + newNovelDownloads += offlineMangaModel + } + downloads += newNovelDownloads + } private fun getMedia(download: Download): Media? { + val type = if (download.type == Download.Type.MANGA) { + "Manga" + } else if (download.type == Download.Type.ANIME) { + "Anime" + } else { + "Novel" + } val directory = File( currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/Manga/${download.title}" + "Dantotsu/$type/${download.title}" ) //load media.json and convert to media class with gson - try { + return try { val gson = GsonBuilder() .registerTypeAdapter(SChapter::class.java, InstanceCreator { SChapterImpl() // Provide an instance of SChapterImpl @@ -189,19 +252,26 @@ class OfflineMangaFragment : Fragment() { .create() val media = File(directory, "media.json") val mediaJson = media.readText() - return gson.fromJson(mediaJson, Media::class.java) + gson.fromJson(mediaJson, Media::class.java) } catch (e: Exception) { logger("Error loading media.json: ${e.message}") logger(e.printStackTrace()) FirebaseCrashlytics.getInstance().recordException(e) - return null + null } } private fun loadOfflineMangaModel(download: Download): OfflineMangaModel { + val type = if (download.type == Download.Type.MANGA) { + "Manga" + } else if (download.type == Download.Type.ANIME) { + "Anime" + } else { + "Novel" + } val directory = File( currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/Manga/${download.title}" + "Dantotsu/$type/${download.title}" ) //load media.json and convert to media class with gson try { @@ -215,8 +285,8 @@ class OfflineMangaFragment : Fragment() { null } val title = mediaModel.nameMAL ?: "unknown" - val score = if (mediaModel.userScore != 0) mediaModel.userScore.toString() else - if (mediaModel.meanScore == null) "?" else mediaModel.meanScore.toString() + val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore + ?: 0) else mediaModel.userScore) / 10.0).toString() val isOngoing = false val isUserScored = mediaModel.userScore != 0 return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri) @@ -227,4 +297,8 @@ class OfflineMangaFragment : Fragment() { return OfflineMangaModel("unknown", "0", false, false, null) } } +} + +interface OfflineMangaSearchListener { + fun onSearchQuery(query: String) } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt index 25158142..e987cd25 100644 --- a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt @@ -174,7 +174,7 @@ class NovelDownloaderService : Service() { notificationManager.notify(NOTIFICATION_ID, builder.build()) } - suspend fun isEpubFile(urlString: String): Boolean { + private suspend fun isEpubFile(urlString: String): Boolean { return withContext(Dispatchers.IO) { try { val request = Request.Builder() @@ -200,122 +200,150 @@ class NovelDownloaderService : Service() { } } + private fun isAlreadyDownloaded(urlString: String): Boolean { + return urlString.contains("file://") + } + suspend fun download(task: DownloadTask) { - withContext(Dispatchers.Main) { - val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - ContextCompat.checkSelfPermission( - this@NovelDownloaderService, - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - } else { - true - } + try { + withContext(Dispatchers.Main) { + val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission( + this@NovelDownloaderService, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } else { + true + } - broadcastDownloadStarted(task.originalLink) + broadcastDownloadStarted(task.originalLink) - if (notifi) { - builder.setContentText("Downloading ${task.title} - ${task.chapter}") - notificationManager.notify(NOTIFICATION_ID, builder.build()) - } + if (notifi) { + builder.setContentText("Downloading ${task.title} - ${task.chapter}") + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } - if (!isEpubFile(task.downloadLink)) { - logger("Download link is not an .epub file") - broadcastDownloadFailed(task.originalLink) - snackString("Download link is not an .epub file") - return@withContext - } + if (!isEpubFile(task.downloadLink)) { + if (isAlreadyDownloaded(task.originalLink)) { + logger("Already downloaded") + broadcastDownloadFinished(task.originalLink) + snackString("Already downloaded") + return@withContext + } + logger("Download link is not an .epub file") + broadcastDownloadFailed(task.originalLink) + snackString("Download link is not an .epub file") + return@withContext + } - // Start the download - withContext(Dispatchers.IO) { - try { - val request = Request.Builder() - .url(task.downloadLink) - .build() + // Start the download + withContext(Dispatchers.IO) { + try { + val request = Request.Builder() + .url(task.downloadLink) + .build() - networkHelper.downloadClient.newCall(request).execute().use { response -> - // Ensure the response is successful and has a body - if (!response.isSuccessful || response.body == null) { - throw IOException("Failed to download file: ${response.message}") - } + networkHelper.downloadClient.newCall(request).execute().use { response -> + // Ensure the response is successful and has a body + if (!response.isSuccessful || response.body == null) { + throw IOException("Failed to download file: ${response.message}") + } - val file = File( - this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/Novel/${task.title}/${task.chapter}/0.epub" - ) + val file = File( + this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Novel/${task.title}/${task.chapter}/0.epub" + ) - // Create directories if they don't exist - file.parentFile?.takeIf { !it.exists() }?.mkdirs() + // Create directories if they don't exist + file.parentFile?.takeIf { !it.exists() }?.mkdirs() - // Overwrite existing file - if (file.exists()) file.delete() + // Overwrite existing file + if (file.exists()) file.delete() - //download cover - task.coverUrl?.let { - file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") } - } + //download cover + task.coverUrl?.let { + file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") } + } - val sink = file.sink().buffer() - val responseBody = response.body - val totalBytes = responseBody.contentLength() - var downloadedBytes = 0L + val sink = file.sink().buffer() + val responseBody = response.body + val totalBytes = responseBody.contentLength() + var downloadedBytes = 0L - val notificationUpdateInterval = 1024 * 1024 // 1 MB - val broadcastUpdateInterval = 1024 * 256 // 256 KB - var lastNotificationUpdate = 0L - var lastBroadcastUpdate = 0L + val notificationUpdateInterval = 1024 * 1024 // 1 MB + val broadcastUpdateInterval = 1024 * 256 // 256 KB + var lastNotificationUpdate = 0L + var lastBroadcastUpdate = 0L - responseBody.source().use { source -> - while (true) { - val read = source.read(sink.buffer, 8192) - if (read == -1L) break - downloadedBytes += read - sink.emit() + responseBody.source().use { source -> + while (true) { + val read = source.read(sink.buffer, 8192) + if (read == -1L) break + downloadedBytes += read + sink.emit() - // Update progress at intervals - if (downloadedBytes - lastNotificationUpdate >= notificationUpdateInterval) { - withContext(Dispatchers.Main) { - val progress = (downloadedBytes * 100 / totalBytes).toInt() - builder.setProgress(100, progress, false) - if (notifi) { - notificationManager.notify( - NOTIFICATION_ID, - builder.build() - ) + // Update progress at intervals + if (downloadedBytes - lastNotificationUpdate >= notificationUpdateInterval) { + withContext(Dispatchers.Main) { + val progress = + (downloadedBytes * 100 / totalBytes).toInt() + builder.setProgress(100, progress, false) + if (notifi) { + notificationManager.notify( + NOTIFICATION_ID, + builder.build() + ) + } } + lastNotificationUpdate = downloadedBytes } - lastNotificationUpdate = downloadedBytes - } - if (downloadedBytes - lastBroadcastUpdate >= broadcastUpdateInterval) { - withContext(Dispatchers.Main) { - val progress = (downloadedBytes * 100 / totalBytes).toInt() - logger("Download progress: $progress") - broadcastDownloadProgress(task.originalLink, progress) + if (downloadedBytes - lastBroadcastUpdate >= broadcastUpdateInterval) { + withContext(Dispatchers.Main) { + val progress = + (downloadedBytes * 100 / totalBytes).toInt() + logger("Download progress: $progress") + broadcastDownloadProgress(task.originalLink, progress) + } + lastBroadcastUpdate = downloadedBytes } - lastBroadcastUpdate = downloadedBytes } } + + sink.close() + //if the file is smaller than 95% of totalBytes, it means the download was interrupted + if (file.length() < totalBytes * 0.95) { + throw IOException("Failed to download file: ${response.message}") + } } - - sink.close() + } catch (e: Exception) { + logger("Exception while downloading .epub inside request: ${e.message}") + throw e } - } catch (e: Exception) { - logger("Exception while downloading .epub: ${e.message}") - snackString("Exception while downloading .epub: ${e.message}") - FirebaseCrashlytics.getInstance().recordException(e) } - } - // Update notification for download completion - builder.setContentText("${task.title} - ${task.chapter} Download complete") - .setProgress(0, 0, false) - if (notifi) { - notificationManager.notify(NOTIFICATION_ID, builder.build()) - } + // Update notification for download completion + builder.setContentText("${task.title} - ${task.chapter} Download complete") + .setProgress(0, 0, false) + if (notifi) { + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } - saveMediaInfo(task) - downloadsManager.addDownload(Download(task.title, task.chapter, Download.Type.NOVEL)) - broadcastDownloadFinished(task.originalLink) - snackString("${task.title} - ${task.chapter} Download finished") + saveMediaInfo(task) + downloadsManager.addDownload( + Download( + task.title, + task.chapter, + Download.Type.NOVEL + ) + ) + broadcastDownloadFinished(task.originalLink) + snackString("${task.title} - ${task.chapter} Download finished") + } + } catch (e: Exception) { + logger("Exception while downloading .epub: ${e.message}") + snackString("Exception while downloading .epub: ${e.message}") + FirebaseCrashlytics.getInstance().recordException(e) + broadcastDownloadFailed(task.originalLink) } } diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt index e6aec94a..35013e02 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt @@ -58,9 +58,12 @@ class MediaDetailsViewModel : ViewModel() { it } if (isDownload) { - data.sourceIndex = when (media.anime != null) { - true -> AnimeSources.list.size - 1 - else -> MangaSources.list.size - 1 + data.sourceIndex = if (media.anime != null) { + AnimeSources.list.size - 1 + } else if (media.format == "MANGA" || media.format == "ONE_SHOT") { + MangaSources.list.size - 1 + } else { + NovelSources.list.size - 1 } } return data diff --git a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt index fd970a34..7154412a 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt @@ -247,7 +247,8 @@ class NovelReadFragment : Fragment(), headerAdapter.progress?.visibility = View.VISIBLE lifecycleScope.launch(Dispatchers.IO) { if (auto || query == "") model.autoSearchNovels(media) - else model.searchNovels(query, source) + //else model.searchNovels(query, source) + else model.autoSearchNovels(media) //testing } searching = true if (save) { diff --git a/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt b/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt index 49fcb5d0..a7817768 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt @@ -1,10 +1,12 @@ package ani.dantotsu.media.novel import android.util.Log +import android.util.TypedValue 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.ItemNovelResponseBinding import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.setAnimation @@ -39,6 +41,10 @@ class NovelResponseAdapter( Glide.with(binding.itemEpisodeImage).load(cover).override(400, 0) .into(binding.itemEpisodeImage) + val typedValue = TypedValue() + fragment.requireContext().theme?.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue, true) + val color = typedValue.data + binding.itemEpisodeTitle.text = novel.name binding.itemEpisodeFiller.text = if (downloadedCheckCallback.downloadedCheck(novel)) { @@ -55,9 +61,7 @@ class NovelResponseAdapter( fragment.requireContext().getColor(android.R.color.holo_green_light) ) } else { - binding.itemEpisodeFiller.setTextColor( - fragment.requireContext().getColor(android.R.color.white) - ) + binding.itemEpisodeFiller.setTextColor(color) } binding.itemEpisodeDesc2.text = novel.extra?.get("1") ?: "" val desc = novel.extra?.get("2") @@ -94,13 +98,21 @@ class NovelResponseAdapter( } binding.root.setOnLongClickListener { - downloadedCheckCallback.deleteDownload(novel) - deleteDownload(novel.link) - snackString("Deleted ${novel.name}") - if (binding.itemEpisodeFiller.text.toString().contains("Download", ignoreCase = true)) { - binding.itemEpisodeFiller.text = "" + val builder = androidx.appcompat.app.AlertDialog.Builder(fragment.requireContext(), R.style.DialogTheme) + builder.setTitle("Delete ${novel.name}?") + builder.setMessage("Are you sure you want to delete ${novel.name}?") + builder.setPositiveButton("Yes") { _, _ -> + downloadedCheckCallback.deleteDownload(novel) + deleteDownload(novel.link) + snackString("Deleted ${novel.name}") + if (binding.itemEpisodeFiller.text.toString().contains("Download", ignoreCase = true)) { + binding.itemEpisodeFiller.text = "" + } } - notifyItemChanged(position) + builder.setNegativeButton("No") { _, _ -> + // Do nothing + } + builder.show() true } } @@ -134,6 +146,8 @@ class NovelResponseAdapter( downloadedChapters.remove(link) val position = list.indexOfFirst { it.link == link } if (position != -1) { + list[position].extra?.remove("0") + list[position].extra?.set("0", "") notifyItemChanged(position) } } @@ -143,6 +157,8 @@ class NovelResponseAdapter( downloadedChapters.remove(link) val position = list.indexOfFirst { it.link == link } if (position != -1) { + list[position].extra?.remove("0") + list[position].extra?.set("0", "Failed") notifyItemChanged(position) } } diff --git a/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt index c9ed1cf1..9a625600 100644 --- a/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt @@ -31,8 +31,20 @@ abstract class NovelParser : BaseParser() { } suspend fun sortedSearch(mediaObj: Media): List { - val query = mediaObj.name ?: mediaObj.nameRomaji - return search(query).sortByVolume(query) + //val query = mediaObj.name ?: mediaObj.nameRomaji + //return search(query).sortByVolume(query) + val results: List + return if(mediaObj.name != null) { + val query = mediaObj.name + results = search(query).sortByVolume(query) + results.ifEmpty { + val q = mediaObj.nameRomaji + search(q).sortByVolume(q) + } + } else { + val query = mediaObj.nameRomaji + search(query).sortByVolume(query) + } } } diff --git a/app/src/main/java/ani/dantotsu/parsers/NovelSources.kt b/app/src/main/java/ani/dantotsu/parsers/NovelSources.kt index 3fdcaae2..e5298bce 100644 --- a/app/src/main/java/ani/dantotsu/parsers/NovelSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/NovelSources.kt @@ -13,11 +13,17 @@ object NovelSources : NovelReadSources() { suspend fun init(fromExtensions: StateFlow>) { // Initialize with the first value from StateFlow val initialExtensions = fromExtensions.first() - list = createParsersFromExtensions(initialExtensions) + list = createParsersFromExtensions(initialExtensions) + Lazier( + { OfflineNovelParser() }, + "Downloaded" + ) // Update as StateFlow emits new values fromExtensions.collect { extensions -> - list = createParsersFromExtensions(extensions) + list = createParsersFromExtensions(extensions) + Lazier( + { OfflineNovelParser() }, + "Downloaded" + ) } } diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt new file mode 100644 index 00000000..ca47b50a --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt @@ -0,0 +1,86 @@ +package ani.dantotsu.parsers + +import android.os.Environment +import ani.dantotsu.currContext +import ani.dantotsu.download.DownloadsManager +import ani.dantotsu.logger +import ani.dantotsu.media.manga.MangaNameAdapter +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import me.xdrop.fuzzywuzzy.FuzzySearch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.File + +class OfflineNovelParser: NovelParser() { + private val downloadManager = Injekt.get() + + override val hostUrl: String = "Offline" + override val name: String = "Offline" + override val saveName: String = "Offline" + + override val volumeRegex = + Regex("vol\\.? (\\d+(\\.\\d+)?)|volume (\\d+(\\.\\d+)?)", RegexOption.IGNORE_CASE) + + override suspend fun loadBook(link: String, extra: Map?): Book { + //link should be a directory + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Novel/$link" + ) + val chapters = mutableListOf() + if (directory.exists()) { + directory.listFiles()?.forEach { + if (it.isDirectory) { + val chapter = Book( + it.name, + it.absolutePath + "/cover.jpg", + null, + listOf(it.absolutePath + "/0.epub") + ) + chapters.add(chapter) + } + } + chapters.sortBy { MangaNameAdapter.findChapterNumber(it.name) } + return chapters.first() + } + return Book( + "error", + "", + null, + listOf("error") + ) + } + + override suspend fun search(query: String): List { + val titles = downloadManager.novelDownloads.map { it.title }.distinct() + val returnTitles: MutableList = mutableListOf() + for (title in titles) { + if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { + returnTitles.add(title) + } + } + val returnList: MutableList = mutableListOf() + for (title in returnTitles) { + //need to search the subdirectories for the ShowResponses + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Novel/$title" + ) + val names = mutableListOf() + if (directory.exists()) { + directory.listFiles()?.forEach { + if (it.isDirectory) { + names.add(it.name) + } + } + } + val cover = currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg" + names.forEach { + returnList.add(ShowResponse(it, it, cover)) + } + } + return returnList + } + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt index b3221425..c733319f 100644 --- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt @@ -41,6 +41,7 @@ class ExtensionsActivity : AppCompatActivity() { val tabLayout = findViewById(R.id.tabLayout) val viewPager = findViewById(R.id.viewPager) + viewPager.offscreenPageLimit = 1 viewPager.adapter = object : FragmentStateAdapter(this) { override fun getItemCount(): Int = 6 @@ -65,13 +66,24 @@ class ExtensionsActivity : AppCompatActivity() { object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab) { searchView.setText("") + searchView.clearFocus() + tabLayout.clearFocus() + viewPager.updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + } } override fun onTabUnselected(tab: TabLayout.Tab) { - // Do nothing + viewPager.updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + } + tabLayout.clearFocus() } override fun onTabReselected(tab: TabLayout.Tab) { + viewPager.updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + } // Do nothing } } diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt index 7b4f5f56..734bb475 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt @@ -11,6 +11,7 @@ import android.widget.ImageView import android.widget.TextView import android.widget.Toast import androidx.core.app.NotificationCompat +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DiffUtil diff --git a/app/src/main/java/ani/dantotsu/settings/NovelExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/NovelExtensionsFragment.kt index f042275d..a700dded 100644 --- a/app/src/main/java/ani/dantotsu/settings/NovelExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/NovelExtensionsFragment.kt @@ -3,6 +3,7 @@ package ani.dantotsu.settings import android.app.NotificationManager import android.content.Context import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -58,6 +59,7 @@ class NovelExtensionsFragment : Fragment(), lifecycleScope.launch { viewModel.pagerFlow.collectLatest { pagingData -> + Log.d("NovelExtensionsFragment", "collectLatest") adapter.submitData(pagingData) } } diff --git a/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt b/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt index 4f94fde6..c3e3b4f4 100644 --- a/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt +++ b/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt @@ -54,7 +54,7 @@ class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() { pref.isIconSpaceReserved = false if (pref is DialogPreference) { pref.dialogTitle = pref.title - //println("pref.dialogTitle: ${pref.dialogTitle}") + //println("pref.dialogTitle: ${pref.dialogTitle}") //TODO: could be useful for dub/sub selection } /*for (entry in sharedPreferences.all.entries) { Log.d("Preferences", "Key: ${entry.key}, Value: ${entry.value}") diff --git a/app/src/main/java/ani/dantotsu/settings/paging/NovelPagingSource.kt b/app/src/main/java/ani/dantotsu/settings/paging/NovelPagingSource.kt index 50fed33d..e8483c62 100644 --- a/app/src/main/java/ani/dantotsu/settings/paging/NovelPagingSource.kt +++ b/app/src/main/java/ani/dantotsu/settings/paging/NovelPagingSource.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest @@ -53,7 +54,13 @@ class NovelExtensionsViewModel( } @OptIn(ExperimentalCoroutinesApi::class) - val pagerFlow: Flow> = searchQuery.flatMapLatest { query -> + val pagerFlow: Flow> = combine( + novelExtensionManager.availableExtensionsFlow, + novelExtensionManager.installedExtensionsFlow, + searchQuery + ) { available, installed, query -> + Triple(available, installed, query) + }.flatMapLatest { (available, installed, query) -> Pager( PagingConfig( pageSize = 15, @@ -61,28 +68,24 @@ class NovelExtensionsViewModel( prefetchDistance = 15 ) ) { - NovelExtensionPagingSource( - novelExtensionManager.availableExtensionsFlow, - novelExtensionManager.installedExtensionsFlow, - searchQuery - ).also { currentPagingSource = it } + NovelExtensionPagingSource(available, installed, query) }.flow }.cachedIn(viewModelScope) } class NovelExtensionPagingSource( - private val availableExtensionsFlow: StateFlow>, - private val installedExtensionsFlow: StateFlow>, - private val searchQuery: StateFlow + private val availableExtensionsFlow: List, + private val installedExtensionsFlow: List, + private val searchQuery: String ) : PagingSource() { override suspend fun load(params: LoadParams): LoadResult { val position = params.key ?: 0 - val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet() + val installedExtensions = installedExtensionsFlow.map { it.pkgName }.toSet() val availableExtensions = - availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions } - val query = searchQuery.first() + availableExtensionsFlow.filterNot { it.pkgName in installedExtensions } + val query = searchQuery val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true val filteredExtensions = if (query.isEmpty()) { availableExtensions diff --git a/app/src/main/res/layout/activity_extensions.xml b/app/src/main/res/layout/activity_extensions.xml index 48876937..5197e60b 100644 --- a/app/src/main/res/layout/activity_extensions.xml +++ b/app/src/main/res/layout/activity_extensions.xml @@ -9,7 +9,7 @@ - - - - + android:layout_weight="1" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_manga_offline.xml b/app/src/main/res/layout/fragment_manga_offline.xml index a8c727f0..995953ea 100644 --- a/app/src/main/res/layout/fragment_manga_offline.xml +++ b/app/src/main/res/layout/fragment_manga_offline.xml @@ -50,9 +50,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" - android:focusable="false" android:fontFamily="@font/poppins_bold" - android:inputType="none" + android:imeOptions="actionSearch" + android:inputType="textPersonName" + android:selectAllOnFocus="true" android:padding="8dp" android:textSize="14sp" tools:ignore="LabelFor,TextContrastCheck" />