basic offline manga fragment
This commit is contained in:
parent
c75df942f2
commit
91d869005c
14 changed files with 502 additions and 32 deletions
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?) {
|
||||||
|
}
|
|
@ -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)) {
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
75
app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt
Normal file
75
app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
78
app/src/main/res/layout/fragment_manga_offline.xml
Normal file
78
app/src/main/res/layout/fragment_manga_offline.xml
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue