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 import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.download.DownloadedType 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(), DownloadTriggerCallback, DownloadedCheckCallback { private var _binding: FragmentAnimeWatchBinding? = null private val binding get() = _binding!! private val model: MediaDetailsViewModel by activityViewModels() private lateinit var media: Media var source = 0 lateinit var novelName: String private lateinit var headerAdapter: NovelReadAdapter private lateinit var novelResponseAdapter: NovelResponseAdapter private var progress = View.VISIBLE private var continueEp: Boolean = false var loaded = false 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.mainName(), 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() if (downloadsManager.queryDownload( DownloadedType( media.mainName(), novel.name, DownloadedType.Type.NOVEL ) ) ) { val file = File( context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "${DownloadsManager.novelLocation}/${media.mainName()}/${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() return downloadsManager.queryDownload( DownloadedType( media.mainName(), novel.name, DownloadedType.Type.NOVEL ) ) } override fun deleteDownload(novel: ShowResponse) { val downloadsManager = Injekt.get() downloadsManager.removeDownload( DownloadedType( media.mainName(), novel.name, DownloadedType.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? = 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()) model.scrolledToTop.observe(viewLifecycleOwner) { if (it) binding.animeSourceRecycler.scrollToPosition(0) } continueEp = model.continueMedia ?: false model.getMedia().observe(viewLifecycleOwner) { if (it != null) { media = it novelName = media.userPreferredName progress = View.GONE binding.mediaInfoProgressBar.visibility = progress if (!loaded) { val sel = media.selected searchQuery = sel?.server ?: media.name ?: media.nameRomaji headerAdapter = NovelReadAdapter(media, this, model.novelSources) 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({ search(searchQuery, sel?.sourceIndex ?: 0, auto = sel?.server == null) }, 100) } } } model.novelResponses.observe(viewLifecycleOwner) { if (it != null) { response = it searching = false novelResponseAdapter.submitList(it) headerAdapter.progress?.visibility = View.GONE } } } lateinit var searchQuery: String private var searching = false fun search(query: String, source: Int, save: Boolean = false, auto: Boolean = false) { if (!searching) { novelResponseAdapter.clear() searchQuery = query headerAdapter.progress?.visibility = View.VISIBLE lifecycleScope.launch(Dispatchers.IO) { if (auto || query == "") model.autoSearchNovels(media) else model.searchNovels(query, source) } searching = true if (save) { val selected = model.loadSelected(media) selected.server = query model.saveSelected(media.id, selected, requireActivity()) } } } fun onSourceChange(i: Int) { val selected = model.loadSelected(media) selected.sourceIndex = i source = i selected.server = null model.saveSelected(media.id, selected, requireActivity()) media.selected = selected } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentAnimeWatchBinding.inflate(inflater, container, false) return _binding?.root } override fun onDestroy() { model.mangaReadSources?.flushText() requireContext().unregisterReceiver(downloadStatusReceiver) super.onDestroy() } private var state: Parcelable? = null override fun onResume() { super.onResume() binding.mediaInfoProgressBar.visibility = progress binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state) } override fun onPause() { 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) }