basic offline manga fragment

This commit is contained in:
Finnley Somdahl 2023-11-05 02:17:49 -06:00
parent c75df942f2
commit 91d869005c
14 changed files with 502 additions and 32 deletions

View file

@ -207,23 +207,21 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
fun isOnline(context: Context): Boolean { fun isOnline(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return tryWith { return tryWith {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) return@tryWith if (cap != null) {
return@tryWith if (cap != null) { when {
when { cap.hasTransport(TRANSPORT_BLUETOOTH) ||
cap.hasTransport(TRANSPORT_BLUETOOTH) || cap.hasTransport(TRANSPORT_CELLULAR) ||
cap.hasTransport(TRANSPORT_CELLULAR) || cap.hasTransport(TRANSPORT_ETHERNET) ||
cap.hasTransport(TRANSPORT_ETHERNET) || cap.hasTransport(TRANSPORT_LOWPAN) ||
cap.hasTransport(TRANSPORT_LOWPAN) || cap.hasTransport(TRANSPORT_USB) ||
cap.hasTransport(TRANSPORT_USB) || cap.hasTransport(TRANSPORT_VPN) ||
cap.hasTransport(TRANSPORT_VPN) || cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI) || cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
else -> false else -> false
} }
} else false } else false
} else true
} ?: false } ?: false
} }
@ -732,7 +730,7 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
if (s != null) { if (s != null) {
(activity ?: currActivity())?.apply { (activity ?: currActivity())?.apply {
runOnUiThread { runOnUiThread {
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_LONG) val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_SHORT)
snackBar.view.apply { snackBar.view.apply {
updateLayoutParams<FrameLayout.LayoutParams> { updateLayoutParams<FrameLayout.LayoutParams> {
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM) gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)

View file

@ -41,6 +41,7 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding import ani.dantotsu.databinding.ActivityMainBinding
import ani.dantotsu.databinding.SplashScreenBinding import ani.dantotsu.databinding.SplashScreenBinding
import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.home.AnimeFragment import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment import ani.dantotsu.home.HomeFragment
import ani.dantotsu.home.LoginFragment import ani.dantotsu.home.LoginFragment

View file

@ -33,6 +33,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PR
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED 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.ACTION_DOWNLOAD_STARTED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
@ -169,6 +170,7 @@ class MangaDownloaderService : Service() {
"Please grant notification permission", "Please grant notification permission",
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
broadcastDownloadFailed(task.chapter)
return@withContext return@withContext
} }
@ -223,14 +225,14 @@ class MangaDownloaderService : Service() {
saveMediaInfo(task) saveMediaInfo(task)
downloadsManager.addDownload(Download(task.title, task.chapter, Download.Type.MANGA)) downloadsManager.addDownload(Download(task.title, task.chapter, Download.Type.MANGA))
downloadsManager.exportDownloads(Download(task.title, task.chapter, Download.Type.MANGA)) //downloadsManager.exportDownloads(Download(task.title, task.chapter, Download.Type.MANGA))
broadcastDownloadFinished(task.chapter) broadcastDownloadFinished(task.chapter)
snackString("${task.title} - ${task.chapter} Download finished") snackString("${task.title} - ${task.chapter} Download finished")
} }
} }
fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) { private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
try { try {
// Define the directory within the private external storage space // Define the directory within the private external storage space
val directory = File( val directory = File(
@ -262,7 +264,7 @@ class MangaDownloaderService : Service() {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
val directory = File( val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${task.title}/${task.chapter}" "Dantotsu/Manga/${task.title}"
) )
if (!directory.exists()) directory.mkdirs() if (!directory.exists()) directory.mkdirs()
@ -272,7 +274,7 @@ class MangaDownloaderService : Service() {
SChapterImpl() // Provide an instance of SChapterImpl SChapterImpl() // Provide an instance of SChapterImpl
}) })
.create() .create()
val mediaJson = gson.toJson(task.sourceMedia) // Assuming sourceMedia is part of DownloadTask val mediaJson = gson.toJson(task.sourceMedia)
val media = gson.fromJson(mediaJson, Media::class.java) val media = gson.fromJson(mediaJson, Media::class.java)
if (media != null) { if (media != null) {
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") } media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
@ -329,6 +331,13 @@ class MangaDownloaderService : Service() {
sendBroadcast(intent) sendBroadcast(intent)
} }
private fun broadcastDownloadFailed(chapterNumber: String) {
val intent = Intent(ACTION_DOWNLOAD_FAILED).apply {
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
}
sendBroadcast(intent)
}
private val cancelReceiver = object : BroadcastReceiver() { private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_CANCEL_DOWNLOAD) { if (intent.action == ACTION_CANCEL_DOWNLOAD) {

View file

@ -0,0 +1,52 @@
package ani.dantotsu.download.manga
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.TextView
import androidx.cardview.widget.CardView
import ani.dantotsu.R
class OfflineMangaAdapter(private val context: Context, private val items: List<OfflineMangaModel>) : BaseAdapter() {
private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
override fun getCount(): Int {
return items.size
}
override fun getItem(position: Int): Any? {
return items[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
var view = convertView
if (view == null) {
view = inflater.inflate(R.layout.item_media_compact, parent, false)
}
val item = getItem(position) as OfflineMangaModel
val imageView = view!!.findViewById<ImageView>(R.id.itemCompactImage)
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
// Bind item data to the views
// For example:
imageView.setImageURI(item.image)
titleTextView.text = item.title
itemScore.text = item.score
if (item.isOngoing) {
ongoing.visibility = View.VISIBLE
} else {
ongoing.visibility = View.GONE
}
return view
}
}

View file

@ -0,0 +1,201 @@
package ani.dantotsu.download.manga
import android.animation.ObjectAnimator
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.OvershootInterpolator
import android.widget.GridView
import androidx.cardview.widget.CardView
import androidx.core.view.updatePaddingRelative
import androidx.fragment.app.Fragment
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.currContext
import ani.dantotsu.databinding.FragmentMangaBinding
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.px
import ani.dantotsu.statusBarHeight
import com.google.firebase.crashlytics.FirebaseCrashlytics
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
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 kotlin.math.max
import kotlin.math.min
class OfflineMangaFragment: Fragment() {
private val downloadManager = Injekt.get<DownloadsManager>()
private var downloads: List<OfflineMangaModel> = listOf()
private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_manga_offline, container, false)
gridView = view.findViewById(R.id.gridView)
getDownloads()
adapter = OfflineMangaAdapter(requireContext(), downloads)
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 }.first()
startActivity(
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(media))
)
}
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var height = statusBarHeight
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) {
height = max(
statusBarHeight,
min(
displayCutout.boundingRects[0].width(),
displayCutout.boundingRects[0].height()
)
)
}
}
}
val refreshLayout = view.findViewById<SwipeRefreshLayout>(R.id.mangaRefresh)
refreshLayout.setSlingshotDistance(height + 128)
refreshLayout.setProgressViewEndTarget(false, height + 128)
refreshLayout.setOnRefreshListener {
Refresh.activity[this.hashCode()]!!.postValue(true)
}
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
var visible = false
fun animate() {
val start = if (visible) 0f else 1f
val end = if (!visible) 0f else 1f
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
}
scrollTop.setOnClickListener {
//TODO: scroll to top
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
super.onPause()
downloads = listOf()
}
override fun onDestroy() {
super.onDestroy()
downloads = listOf()
}
override fun onStop() {
super.onStop()
downloads = listOf()
}
private fun getDownloads() {
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
val newDownloads = mutableListOf<OfflineMangaModel>()
for (title in titles) {
val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newDownloads += offlineMangaModel
}
downloads = newDownloads
}
private fun getMedia(download: Download): Media? {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}"
)
//load media.json and convert to media class with gson
try {
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
return 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
}
}
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel{
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}"
)
//load media.json and convert to media class with gson
try {
val media = File(directory, "media.json")
val mediaJson = media.readText()
val mediaModel = getMedia(download)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else {
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 isOngoing = false
val isUserScored = mediaModel.userScore != 0
return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri)
}
catch (e: Exception){
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
return OfflineMangaModel("unknown", "0", false, false, null)
}
}
}

View file

@ -0,0 +1,6 @@
package ani.dantotsu.download.manga
import android.net.Uri
data class OfflineMangaModel(val title: String, val score: String, val isOngoing: Boolean, val isUserScored: Boolean, val image: Uri?) {
}

View file

@ -80,6 +80,16 @@ class MangaChapterAdapter(
} }
} }
fun removeDownload(chapterNumber: String) {
activeDownloads.remove(chapterNumber)
downloadedChapters.remove(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
notifyItemChanged(position)
}
}
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : RecyclerView.ViewHolder(binding.root) { inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(chapterNumber: String) { fun bind(chapterNumber: String) {
if (activeDownloads.contains(chapterNumber)) { if (activeDownloads.contains(chapterNumber)) {

View file

@ -417,6 +417,12 @@ open class MangaReadFragment : Fragment() {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER) val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
chapterNumber?.let { chapterAdapter.stopDownload(it) } chapterNumber?.let { chapterAdapter.stopDownload(it) }
} }
ACTION_DOWNLOAD_FAILED -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
chapterNumber?.let {
chapterAdapter.removeDownload(it)
}
}
} }
} }
} }
@ -468,6 +474,7 @@ open class MangaReadFragment : Fragment() {
companion object { companion object {
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED" const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED" const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
const val EXTRA_CHAPTER_NUMBER = "extra_chapter_number" const val EXTRA_CHAPTER_NUMBER = "extra_chapter_number"
} }
} }

View file

@ -50,7 +50,7 @@ abstract class BaseParser {
* Isn't necessary to override, but recommended, if you want to improve auto search results * Isn't necessary to override, but recommended, if you want to improve auto search results
* **/ * **/
open suspend fun autoSearch(mediaObj: Media): ShowResponse? { open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
var response = loadSavedShowResponse(mediaObj.id) var response: ShowResponse? = null//loadSavedShowResponse(mediaObj.id)
if (response != null) { if (response != null) {
saveShowResponse(mediaObj.id, response, true) saveShowResponse(mediaObj.id, response, true)
} else { } else {

View file

@ -7,6 +7,7 @@ import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.source.model.SManga
abstract class WatchSources : BaseSources() { abstract class WatchSources : BaseSources() {
@ -23,14 +24,28 @@ abstract class WatchSources : BaseSources() {
} ?: mutableMapOf() } ?: mutableMapOf()
} }
suspend fun loadEpisodes(i: Int, showLink: String, extra: Map<String, String>?, sAnime: SAnime?): MutableMap<String, Episode> { suspend fun loadEpisodes(
i: Int,
showLink: String,
extra: Map<String, String>?,
sAnime: SAnime?
): MutableMap<String, Episode> {
println("finder333 $showLink") println("finder333 $showLink")
val map = mutableMapOf<String, Episode>() val map = mutableMapOf<String, Episode>()
val parser = get(i) val parser = get(i)
tryWithSuspend(true) { tryWithSuspend(true) {
if (sAnime != null) { if (sAnime != null) {
parser.loadEpisodes(showLink,extra, sAnime).forEach { parser.loadEpisodes(showLink, extra, sAnime).forEach {
map[it.number] = Episode(it.number, it.link, it.title, it.description, it.thumbnail, it.isFiller, extra = it.extra, sEpisode = it.sEpisode) map[it.number] = Episode(
it.number,
it.link,
it.title,
it.description,
it.thumbnail,
it.isFiller,
extra = it.extra,
sEpisode = it.sEpisode
)
} }
} }
} }
@ -42,7 +57,7 @@ abstract class WatchSources : BaseSources() {
abstract class MangaReadSources : BaseSources() { abstract class MangaReadSources : BaseSources() {
override operator fun get(i: Int): MangaParser { override operator fun get(i: Int): MangaParser {
return (list.getOrNull(i)?:list.firstOrNull())?.get?.value as? MangaParser return (list.getOrNull(i) ?: list.firstOrNull())?.get?.value as? MangaParser
?: EmptyMangaParser() ?: EmptyMangaParser()
} }
@ -56,6 +71,7 @@ abstract class MangaReadSources : BaseSources() {
suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap<String, MangaChapter> { suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap<String, MangaChapter> {
val map = mutableMapOf<String, MangaChapter>() val map = mutableMapOf<String, MangaChapter>()
val parser = get(i) val parser = get(i)
show.sManga?.let { sManga -> show.sManga?.let { sManga ->
tryWithSuspend(true) { tryWithSuspend(true) {
parser.loadChapters(show.link, show.extra, sManga).forEach { parser.loadChapters(show.link, show.extra, sManga).forEach {
@ -63,15 +79,28 @@ abstract class MangaReadSources : BaseSources() {
} }
} }
} }
if(show.sManga == null) { //must be downloaded
if (show.sManga == null) {
logger("sManga is null") logger("sManga is null")
} }
if (parser is OfflineMangaParser && show.sManga == null) {
tryWithSuspend(true) {
// Since we've checked, we can safely cast parser to OfflineMangaParser and call its methods
parser.loadChapters(show.link, show.extra, SManga.create()).forEach {
map[it.number] = MangaChapter(it)
}
}
} else {
logger("Parser is not an instance of OfflineMangaParser")
}
logger("map size ${map.size}") logger("map size ${map.size}")
return map return map
} }
} }
abstract class NovelReadSources : BaseSources(){ abstract class NovelReadSources : BaseSources() {
override operator fun get(i: Int): NovelParser? { override operator fun get(i: Int): NovelParser? {
return if (list.isNotEmpty()) { return if (list.isNotEmpty()) {
(list.getOrNull(i) ?: list[0]).get.value as NovelParser (list.getOrNull(i) ?: list[0]).get.value as NovelParser
@ -87,7 +116,7 @@ class EmptyNovelParser : NovelParser() {
override val volumeRegex: Regex = Regex("") override val volumeRegex: Regex = Regex("")
override suspend fun loadBook(link: String, extra: Map<String, String>?): Book { override suspend fun loadBook(link: String, extra: Map<String, String>?): Book {
return Book("","", null, emptyList()) // Return an empty Book object or some default value return Book("", "", null, emptyList()) // Return an empty Book object or some default value
} }
override suspend fun search(query: String): List<ShowResponse> { override suspend fun search(query: String): List<ShowResponse> {

View file

@ -7,16 +7,19 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
object MangaSources : MangaReadSources() { object MangaSources : MangaReadSources() {
// Instantiate the static parser
private val offlineMangaParser by lazy { OfflineMangaParser() }
override var list: List<Lazier<BaseParser>> = emptyList() override var list: List<Lazier<BaseParser>> = emptyList()
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) { suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) {
// Initialize with the first value from StateFlow // Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first() val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions) list = createParsersFromExtensions(initialExtensions) + Lazier({ OfflineMangaParser() }, "Downloaded")
// Update as StateFlow emits new values // Update as StateFlow emits new values
fromExtensions.collect { extensions -> fromExtensions.collect { extensions ->
list = createParsersFromExtensions(extensions) list = createParsersFromExtensions(extensions) + Lazier({ OfflineMangaParser() }, "Downloaded")
} }
} }

View file

@ -0,0 +1,75 @@
package ani.dantotsu.parsers
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
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 OfflineMangaParser: MangaParser() {
private val downloadManager = Injekt.get<DownloadsManager>()
override val hostUrl: String = "Offline"
override val name: String = "Offline"
override val saveName: String = "Offline"
override suspend fun loadChapters(
mangaLink: String,
extra: Map<String, String>?,
sManga: SManga
): List<MangaChapter> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$mangaLink"
)
//get all of the folder names and add them to the list
val chapters = mutableListOf<MangaChapter>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isDirectory) {
val chapter = MangaChapter(it.name, "$mangaLink/${it.name}", it.name, null, SChapter.create())
chapters.add(chapter)
}
}
return chapters
}
return emptyList()
}
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$chapterLink"
)
val images = mutableListOf<MangaImage>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isFile) {
val image = MangaImage(it.absolutePath, false, null)
images.add(image)
}
}
return images
}
return emptyList()
}
override suspend fun search(query: String): List<ShowResponse> {
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
returnTitles.add(title)
}
}
val returnList: MutableList<ShowResponse> = mutableListOf()
for (title in returnTitles) {
returnList.add(ShowResponse(title, title, title))
}
return returnList
}
}

View file

@ -20,6 +20,7 @@
android:gravity="center" android:gravity="center"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/noInternetSad" android:id="@+id/noInternetSad"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -49,7 +50,7 @@
android:layout_width="150dp" android:layout_width="150dp"
android:layout_height="64dp" android:layout_height="64dp"
android:layout_gravity="bottom|center_horizontal" android:layout_gravity="bottom|center_horizontal"
android:layout_margin="32dp" android:layout_margin="128dp"
android:fontFamily="@font/poppins_bold" android:fontFamily="@font/poppins_bold"
android:text="@string/refresh" android:text="@string/refresh"
app:cornerRadius="16dp" app:cornerRadius="16dp"

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".home.MangaFragment">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/mangaRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false">
<!-- Add a LinearLayout or other ViewGroup as a direct child of SwipeRefreshLayout -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/mangaOfflineTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:text="Offline Manga"
android:textColor="?attr/colorOnSurface"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="@font/poppins"
android:gravity="center_horizontal" />
<!-- This TextView might overlap with GridView if GridView has items -->
<TextView
android:id="@+id/noMangaOffline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="No offline manga found"
android:textColor="?attr/colorOnSurface"
android:textSize="18sp"
android:visibility="gone" />
<GridView
android:id="@+id/gridView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:numColumns="auto_fit"
android:columnWidth="108dp"
android:verticalSpacing="10dp"
android:horizontalSpacing="10dp"
android:padding="10dp"
android:gravity="center" />
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.cardview.widget.CardView
android:id="@+id/mangaPageScrollTop"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="?android:colorBackground"
app:cardCornerRadius="24dp"
app:contentPadding="12dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:rotation="90"
app:srcCompat="@drawable/ic_round_arrow_back_ios_new_24"
app:tint="?attr/colorOnSurface"
tools:ignore="ContentDescription" />
</androidx.cardview.widget.CardView>
</FrameLayout>