Light novel support

This commit is contained in:
Finnley Somdahl 2023-11-30 03:41:45 -06:00
parent 32f918450a
commit c7bc1ffe9e
39 changed files with 2537 additions and 91 deletions

View file

@ -30,7 +30,7 @@ import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.ServiceDataSingleton
import ani.dantotsu.download.manga.MangaServiceDataSingleton
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
@ -408,15 +408,15 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
simultaneousDownloads = 2
)
ServiceDataSingleton.downloadQueue.offer(downloadTask)
MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
// If the service is not already running, start it
if (!ServiceDataSingleton.isServiceRunning) {
if (!MangaServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, MangaDownloaderService::class.java)
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(requireContext(), intent)
}
ServiceDataSingleton.isServiceRunning = true
MangaServiceDataSingleton.isServiceRunning = true
}
// Inform the adapter that the download has started
@ -456,6 +456,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if(!this@MangaReadFragment::chapterAdapter.isInitialized) return
when (intent.action) {
ACTION_DOWNLOAD_STARTED -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)

View file

@ -29,6 +29,14 @@ class BookDialog : BottomSheetDialogFragment() {
private lateinit var novel: ShowResponse
private var source:Int = 0
interface Callback {
fun onDownloadTriggered(link: String)
}
private var callback: Callback? = null
fun setCallback(callback: Callback) {
this.callback = callback
}
override fun onCreate(savedInstanceState: Bundle?) {
arguments?.let {
novelName = it.getString("novelName")!!
@ -51,7 +59,7 @@ class BookDialog : BottomSheetDialogFragment() {
binding.itemBookTitle.text = it.name
binding.itemBookDesc.text = it.description
binding.itemBookImage.loadImage(it.img)
binding.bookRecyclerView.adapter = UrlAdapter(it.links, it, novelName)
binding.bookRecyclerView.adapter = UrlAdapter(it.links, it, novelName, callback)
}
}
lifecycleScope.launch(Dispatchers.IO) {

View file

@ -64,7 +64,7 @@ class NovelReadAdapter(
binding.searchBar.setEndIconOnClickListener { search() }
}
override fun getItemCount(): Int = 0
override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemNovelHeaderBinding) : RecyclerView.ViewHolder(binding.root)
}

View file

@ -1,12 +1,20 @@
package ani.dantotsu.media.novel
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@ -14,16 +22,29 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.novel.NovelDownloaderService
import ani.dantotsu.download.novel.NovelServiceDataSingleton
import ani.dantotsu.loadData
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.novel.novelreader.NovelReaderActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.saveData
import ani.dantotsu.settings.UserInterfaceSettings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class NovelReadFragment : Fragment() {
class NovelReadFragment : Fragment(),
DownloadTriggerCallback,
DownloadedCheckCallback {
private var _binding: FragmentAnimeWatchBinding? = null
private val binding get() = _binding!!
@ -42,9 +63,104 @@ class NovelReadFragment : Fragment() {
val uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) {
Log.e("downloadTrigger", novelDownloadPackage.link)
val downloadTask = NovelDownloaderService.DownloadTask(
title = media.nameMAL ?: media.nameRomaji,
chapter = novelDownloadPackage.novelName,
downloadLink = novelDownloadPackage.link,
originalLink = novelDownloadPackage.originalLink,
sourceMedia = media,
coverUrl = novelDownloadPackage.coverUrl,
retries = 2,
)
NovelServiceDataSingleton.downloadQueue.offer(downloadTask)
CoroutineScope(Dispatchers.IO).launch {
if (!NovelServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, NovelDownloaderService::class.java)
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(requireContext(), intent)
}
NovelServiceDataSingleton.isServiceRunning = true
}
}
}
override fun downloadedCheckWithStart(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get<DownloadsManager>()
if(downloadsManager.queryDownload(Download(media.nameMAL ?: media.nameRomaji, novel.name, Download.Type.NOVEL))) {
val file = File(context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "${DownloadsManager.novelLocation}/${media.nameMAL ?: media.nameRomaji}/${novel.name}/0.epub")
if (!file.exists()) return false
val fileUri = FileProvider.getUriForFile(requireContext(), "${requireContext().packageName}.provider", file)
val intent = Intent(context, NovelReaderActivity::class.java).apply {
action = Intent.ACTION_VIEW
setDataAndType(fileUri, "application/epub+zip")
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
startActivity(intent)
return true
} else {
return false
}
}
override fun downloadedCheck(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get<DownloadsManager>()
return downloadsManager.queryDownload(Download(media.nameMAL ?: media.nameRomaji, novel.name, Download.Type.NOVEL))
}
override fun deleteDownload(novel: ShowResponse) {
val downloadsManager = Injekt.get<DownloadsManager>()
downloadsManager.removeDownload(Download(media.nameMAL ?: media.nameRomaji, novel.name, Download.Type.NOVEL))
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!this@NovelReadFragment::novelResponseAdapter.isInitialized) return
when (intent.action) {
ACTION_DOWNLOAD_STARTED -> {
val link = intent.getStringExtra(EXTRA_NOVEL_LINK)
link?.let {
novelResponseAdapter.startDownload(it)
}
}
ACTION_DOWNLOAD_FINISHED -> {
val link = intent.getStringExtra(EXTRA_NOVEL_LINK)
link?.let {
novelResponseAdapter.stopDownload(it)
}
}
ACTION_DOWNLOAD_FAILED -> {
val link = intent.getStringExtra(EXTRA_NOVEL_LINK)
link?.let {
novelResponseAdapter.purgeDownload(it)
}
}
ACTION_DOWNLOAD_PROGRESS -> {
val link = intent.getStringExtra(EXTRA_NOVEL_LINK)
val progress = intent.getIntExtra("progress", 0)
link?.let {
novelResponseAdapter.updateDownloadProgress(it, progress)
}
}
}
}
}
var response: List<ShowResponse>? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val intentFilter = IntentFilter().apply {
addAction(ACTION_DOWNLOAD_STARTED)
addAction(ACTION_DOWNLOAD_FINISHED)
addAction(ACTION_DOWNLOAD_FAILED)
addAction(ACTION_DOWNLOAD_PROGRESS)
}
ContextCompat.registerReceiver(requireContext(), downloadStatusReceiver, intentFilter ,ContextCompat.RECEIVER_EXPORTED)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
binding.animeSourceRecycler.layoutManager = LinearLayoutManager(requireContext())
@ -63,7 +179,7 @@ class NovelReadFragment : Fragment() {
val sel = media.selected
searchQuery = sel?.server ?: media.name ?: media.nameRomaji
headerAdapter = NovelReadAdapter(media, this, model.novelSources)
novelResponseAdapter = NovelResponseAdapter(this)
novelResponseAdapter = NovelResponseAdapter(this, this, this) // probably a better way to do this but it works
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, novelResponseAdapter)
loaded = true
Handler(Looper.getMainLooper()).postDelayed({
@ -74,6 +190,7 @@ class NovelReadFragment : Fragment() {
}
model.novelResponses.observe(viewLifecycleOwner) {
if (it != null) {
response = it
searching = false
novelResponseAdapter.submitList(it)
headerAdapter.progress?.visibility = View.GONE
@ -121,6 +238,7 @@ class NovelReadFragment : Fragment() {
override fun onDestroy() {
model.mangaReadSources?.flushText()
requireContext().unregisterReceiver(downloadStatusReceiver)
super.onDestroy()
}
@ -135,4 +253,22 @@ class NovelReadFragment : Fragment() {
super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
}
companion object {
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
const val EXTRA_NOVEL_LINK = "extra_novel_link"
}
}
interface DownloadTriggerCallback {
fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage)
}
interface DownloadedCheckCallback {
fun downloadedCheck(novel: ShowResponse): Boolean
fun downloadedCheckWithStart(novel: ShowResponse): Boolean
fun deleteDownload(novel: ShowResponse)
}

View file

@ -1,5 +1,6 @@
package ani.dantotsu.media.novel
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -7,16 +8,23 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemNovelResponseBinding
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.setAnimation
import ani.dantotsu.snackString
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
class NovelResponseAdapter(val fragment: NovelReadFragment) : RecyclerView.Adapter<NovelResponseAdapter.ViewHolder>() {
class NovelResponseAdapter(
val fragment: NovelReadFragment,
val downloadTriggerCallback: DownloadTriggerCallback,
val downloadedCheckCallback: DownloadedCheckCallback
) : RecyclerView.Adapter<NovelResponseAdapter.ViewHolder>() {
val list: MutableList<ShowResponse> = mutableListOf()
inner class ViewHolder(val binding: ItemNovelResponseBinding) : RecyclerView.ViewHolder(binding.root)
inner class ViewHolder(val binding: ItemNovelResponseBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind = ItemNovelResponseBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val bind =
ItemNovelResponseBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(bind)
}
@ -27,19 +35,128 @@ class NovelResponseAdapter(val fragment: NovelReadFragment) : RecyclerView.Adapt
val novel = list[position]
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val cover = GlideUrl(novel.coverUrl.url){ novel.coverUrl.headers }
Glide.with(binding.itemEpisodeImage).load(cover).override(400,0).into(binding.itemEpisodeImage)
val cover = GlideUrl(novel.coverUrl.url) { novel.coverUrl.headers }
Glide.with(binding.itemEpisodeImage).load(cover).override(400, 0)
.into(binding.itemEpisodeImage)
binding.itemEpisodeTitle.text = novel.name
binding.itemEpisodeFiller.text = novel.extra?.get("0") ?: ""
binding.itemEpisodeFiller.text =
if (downloadedCheckCallback.downloadedCheck(novel)) {
"Downloaded"
} else {
novel.extra?.get("0") ?: ""
}
if (binding.itemEpisodeFiller.text.contains("Downloading")) {
binding.itemEpisodeFiller.setTextColor(
fragment.requireContext().getColor(android.R.color.holo_blue_light)
)
} else if (binding.itemEpisodeFiller.text.contains("Downloaded")) {
binding.itemEpisodeFiller.setTextColor(
fragment.requireContext().getColor(android.R.color.holo_green_light)
)
} else {
binding.itemEpisodeFiller.setTextColor(
fragment.requireContext().getColor(android.R.color.white)
)
}
binding.itemEpisodeDesc2.text = novel.extra?.get("1") ?: ""
val desc = novel.extra?.get("2")
binding.itemEpisodeDesc.visibility = if (desc != null && desc.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.visibility =
if (desc != null && desc.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.text = desc ?: ""
binding.root.setOnClickListener {
BookDialog.newInstance(fragment.novelName, novel, fragment.source)
.show(fragment.parentFragmentManager, "dialog")
//make sure the file is not downloading
if (activeDownloads.contains(novel.link)) {
return@setOnClickListener
}
if (downloadedCheckCallback.downloadedCheckWithStart(novel)) {
return@setOnClickListener
}
val bookDialog = BookDialog.newInstance(fragment.novelName, novel, fragment.source)
bookDialog.setCallback(object : BookDialog.Callback {
override fun onDownloadTriggered(link: String) {
downloadTriggerCallback.downloadTrigger(
NovelDownloadPackage(
link,
novel.coverUrl.url,
novel.name,
novel.link
)
)
bookDialog.dismiss()
}
})
bookDialog.show(fragment.parentFragmentManager, "dialog")
}
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 = ""
}
notifyItemChanged(position)
true
}
}
private val activeDownloads = mutableSetOf<String>()
private val downloadedChapters = mutableSetOf<String>()
fun startDownload(link: String) {
activeDownloads.add(link)
val position = list.indexOfFirst { it.link == link }
if (position != -1) {
list[position].extra?.remove("0")
list[position].extra?.set("0", "Downloading: 0%")
notifyItemChanged(position)
}
}
fun stopDownload(link: String) {
activeDownloads.remove(link)
downloadedChapters.add(link)
val position = list.indexOfFirst { it.link == link }
if (position != -1) {
list[position].extra?.remove("0")
list[position].extra?.set("0", "Downloaded")
notifyItemChanged(position)
}
}
fun deleteDownload(link: String) { //TODO:
downloadedChapters.remove(link)
val position = list.indexOfFirst { it.link == link }
if (position != -1) {
notifyItemChanged(position)
}
}
fun purgeDownload(link: String) {
activeDownloads.remove(link)
downloadedChapters.remove(link)
val position = list.indexOfFirst { it.link == link }
if (position != -1) {
notifyItemChanged(position)
}
}
fun updateDownloadProgress(link: String, progress: Int) {
if (!activeDownloads.contains(link)) {
activeDownloads.add(link)
}
val position = list.indexOfFirst { it.link == link }
if (position != -1) {
list[position].extra?.remove("0")
list[position].extra?.set("0", "Downloading: $progress%")
Log.d("NovelResponseAdapter", "updateDownloadProgress: $progress, position: $position")
notifyItemChanged(position)
}
}
@ -54,4 +171,11 @@ class NovelResponseAdapter(val fragment: NovelReadFragment) : RecyclerView.Adapt
list.clear()
notifyItemRangeRemoved(0, size)
}
}
}
data class NovelDownloadPackage(
val link: String,
val coverUrl: String,
val novelName: String,
val originalLink: String
)

View file

@ -9,12 +9,11 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.FileUrl
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemUrlBinding
import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.Book
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.tryWith
class UrlAdapter(private val urls: List<FileUrl>, val book: Book, val novel: String) :
class UrlAdapter(private val urls: List<FileUrl>, val book: Book, val novel: String, val callback: BookDialog.Callback?) :
RecyclerView.Adapter<UrlAdapter.UrlViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UrlViewHolder {
@ -26,6 +25,7 @@ class UrlAdapter(private val urls: List<FileUrl>, val book: Book, val novel: Str
val binding = holder.binding
val url = urls[position]
binding.urlQuality.text = url.url
binding.urlQuality.maxLines = 4
binding.urlDownload.visibility = View.VISIBLE
}
@ -36,12 +36,14 @@ class UrlAdapter(private val urls: List<FileUrl>, val book: Book, val novel: Str
itemView.setSafeOnClickListener {
tryWith(true) {
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
download(
callback?.onDownloadTriggered(book.links[bindingAdapterPosition].url)
/*download(
itemView.context,
book,
bindingAdapterPosition,
novel
)
)*/
}
}
itemView.setOnLongClickListener {

View file

@ -138,7 +138,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
ThemeManager(this).applyTheme()
binding = ActivityNovelReaderBinding.inflate(layoutInflater)
setContentView(binding.root)