Initial commit
This commit is contained in:
commit
21bfbfb139
520 changed files with 47819 additions and 0 deletions
77
app/src/main/java/ani/dantotsu/media/novel/BookDialog.kt
Normal file
77
app/src/main/java/ani/dantotsu/media/novel/BookDialog.kt
Normal file
|
@ -0,0 +1,77 @@
|
|||
package ani.dantotsu.media.novel
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.databinding.BottomSheetBookBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class BookDialog : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetBookBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val viewList = mutableListOf<View>()
|
||||
|
||||
private val viewModel by activityViewModels<MediaDetailsViewModel>()
|
||||
|
||||
private lateinit var novelName:String
|
||||
private lateinit var novel: ShowResponse
|
||||
private var source:Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
arguments?.let {
|
||||
novelName = it.getString("novelName")!!
|
||||
novel = it.getSerialized("novel")!!
|
||||
source = it.getInt("source")
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = BottomSheetBookBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.bookRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
viewModel.book.observe(viewLifecycleOwner) {
|
||||
if(it!=null){
|
||||
binding.itemBookTitle.text = it.name
|
||||
binding.itemBookDesc.text = it.description
|
||||
binding.itemBookImage.loadImage(it.img)
|
||||
binding.bookRecyclerView.adapter = UrlAdapter(it.links, it, novelName)
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
viewModel.loadBook(novel, source)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(novelName:String, novel:ShowResponse, source: Int) : BookDialog{
|
||||
val bundle = Bundle().apply {
|
||||
putString("novelName", novelName)
|
||||
putInt("source", source)
|
||||
putSerializable("novel", novel)
|
||||
}
|
||||
return BookDialog().apply {
|
||||
arguments = bundle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package ani.dantotsu.media.novel
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_SEARCH
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ItemNovelHeaderBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.parsers.NovelReadSources
|
||||
|
||||
class NovelReadAdapter(
|
||||
private val media: Media,
|
||||
private val fragment: NovelReadFragment,
|
||||
private val novelReadSources: NovelReadSources
|
||||
) : RecyclerView.Adapter<NovelReadAdapter.ViewHolder>() {
|
||||
|
||||
var progress: View? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NovelReadAdapter.ViewHolder {
|
||||
val binding = ItemNovelHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
progress = binding.progress.root
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
private val imm = fragment.requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
progress = binding.progress.root
|
||||
|
||||
fun search(): Boolean {
|
||||
val query = binding.searchBarText.text.toString()
|
||||
val source = media.selected!!.source.let { if (it >= novelReadSources.names.size) 0 else it }
|
||||
fragment.source = source
|
||||
|
||||
binding.searchBarText.clearFocus()
|
||||
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
||||
fragment.search(query, source, true)
|
||||
return true
|
||||
}
|
||||
|
||||
val source = media.selected!!.source.let { if (it >= novelReadSources.names.size) 0 else it }
|
||||
if (novelReadSources.names.isNotEmpty() && source in 0 until novelReadSources.names.size) {
|
||||
binding.animeSource.setText(novelReadSources.names[source], false)
|
||||
}
|
||||
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, novelReadSources.names))
|
||||
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
||||
fragment.onSourceChange(i)
|
||||
search()
|
||||
}
|
||||
|
||||
binding.searchBarText.setText(fragment.searchQuery)
|
||||
binding.searchBarText.setOnEditorActionListener { _, actionId, _ ->
|
||||
return@setOnEditorActionListener when (actionId) {
|
||||
IME_ACTION_SEARCH -> search()
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
binding.searchBar.setEndIconOnClickListener { search() }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 0
|
||||
|
||||
inner class ViewHolder(val binding: ItemNovelHeaderBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
}
|
138
app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt
Normal file
138
app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt
Normal file
|
@ -0,0 +1,138 @@
|
|||
package ani.dantotsu.media.novel
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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.loadData
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NovelReadFragment : Fragment() {
|
||||
|
||||
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 onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
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)
|
||||
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, novelResponseAdapter)
|
||||
loaded = true
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
search(searchQuery, sel?.source ?: 0, auto = sel?.server == null)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
model.novelResponses.observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
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.source = 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()
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package ani.dantotsu.media.novel
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.databinding.ItemNovelResponseBinding
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.setAnimation
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
|
||||
class NovelResponseAdapter(val fragment: NovelReadFragment) : RecyclerView.Adapter<NovelResponseAdapter.ViewHolder>() {
|
||||
val list: MutableList<ShowResponse> = mutableListOf()
|
||||
|
||||
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)
|
||||
return ViewHolder(bind)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = list.size
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
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)
|
||||
|
||||
binding.itemEpisodeTitle.text = novel.name
|
||||
binding.itemEpisodeFiller.text = novel.extra?.get("0") ?: ""
|
||||
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.text = desc ?: ""
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
BookDialog.newInstance(fragment.novelName, novel, fragment.source)
|
||||
.show(fragment.parentFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
|
||||
fun submitList(it: List<ShowResponse>) {
|
||||
val old = list.size
|
||||
list.addAll(it)
|
||||
notifyItemRangeInserted(old, it.size)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
val size = list.size
|
||||
list.clear()
|
||||
notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
}
|
54
app/src/main/java/ani/dantotsu/media/novel/UrlAdapter.kt
Normal file
54
app/src/main/java/ani/dantotsu/media/novel/UrlAdapter.kt
Normal file
|
@ -0,0 +1,54 @@
|
|||
package ani.dantotsu.media.novel
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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) :
|
||||
RecyclerView.Adapter<UrlAdapter.UrlViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UrlViewHolder {
|
||||
return UrlViewHolder(ItemUrlBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val url = urls[position]
|
||||
binding.urlQuality.text = url.url
|
||||
binding.urlDownload.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = urls.size
|
||||
|
||||
inner class UrlViewHolder(val binding: ItemUrlBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setSafeOnClickListener {
|
||||
tryWith(true) {
|
||||
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
download(
|
||||
itemView.context,
|
||||
book,
|
||||
bindingAdapterPosition,
|
||||
novel
|
||||
)
|
||||
}
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
val file = urls[bindingAdapterPosition]
|
||||
copyToClipboard(file.url, true)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,435 @@
|
|||
package ani.dantotsu.media.novel.novelreader
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.AdapterView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.GesturesListener
|
||||
import ani.dantotsu.NoPaddingArrayAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ActivityNovelReaderBinding
|
||||
import ani.dantotsu.hideSystemBars
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.CurrentNovelReaderSettings
|
||||
import ani.dantotsu.settings.CurrentReaderSettings
|
||||
import ani.dantotsu.settings.NovelReaderSettings
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWith
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.vipulog.ebookreader.Book
|
||||
import com.vipulog.ebookreader.EbookReaderEventListener
|
||||
import com.vipulog.ebookreader.ReaderError
|
||||
import com.vipulog.ebookreader.ReaderFlow
|
||||
import com.vipulog.ebookreader.ReaderTheme
|
||||
import com.vipulog.ebookreader.RelocationInfo
|
||||
import com.vipulog.ebookreader.TocItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
||||
class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
private lateinit var binding: ActivityNovelReaderBinding
|
||||
private val scope = lifecycleScope
|
||||
|
||||
lateinit var settings: NovelReaderSettings
|
||||
private lateinit var uiSettings: UserInterfaceSettings
|
||||
|
||||
private var notchHeight: Int? = null
|
||||
|
||||
var loaded = false
|
||||
|
||||
private lateinit var book: Book
|
||||
private lateinit var sanitizedBookId: String
|
||||
private lateinit var toc: List<TocItem>
|
||||
private var currentTheme: ReaderTheme? = null
|
||||
private var currentCfi: String? = null
|
||||
|
||||
val themes = ArrayList<ReaderTheme>()
|
||||
|
||||
|
||||
init {
|
||||
val forestTheme = ReaderTheme(
|
||||
name = "Forest",
|
||||
lightFg = Color.parseColor("#000000"),
|
||||
lightBg = Color.parseColor("#E7F6E7"),
|
||||
lightLink = Color.parseColor("#008000"),
|
||||
darkFg = Color.parseColor("#FFFFFF"),
|
||||
darkBg = Color.parseColor("#084D08"),
|
||||
darkLink = Color.parseColor("#00B200")
|
||||
)
|
||||
|
||||
val oceanTheme = ReaderTheme(
|
||||
name = "Ocean",
|
||||
lightFg = Color.parseColor("#000000"),
|
||||
lightBg = Color.parseColor("#E4F0F9"),
|
||||
lightLink = Color.parseColor("#007BFF"),
|
||||
darkFg = Color.parseColor("#FFFFFF"),
|
||||
darkBg = Color.parseColor("#0A2E3E"),
|
||||
darkLink = Color.parseColor("#00A5E4")
|
||||
)
|
||||
|
||||
val sunsetTheme = ReaderTheme(
|
||||
name = "Sunset",
|
||||
lightFg = Color.parseColor("#000000"),
|
||||
lightBg = Color.parseColor("#FDEDE6"),
|
||||
lightLink = Color.parseColor("#FF5733"),
|
||||
darkFg = Color.parseColor("#FFFFFF"),
|
||||
darkBg = Color.parseColor("#441517"),
|
||||
darkLink = Color.parseColor("#FF6B47")
|
||||
)
|
||||
|
||||
val desertTheme = ReaderTheme(
|
||||
name = "Desert",
|
||||
lightFg = Color.parseColor("#000000"),
|
||||
lightBg = Color.parseColor("#FDF5E6"),
|
||||
lightLink = Color.parseColor("#FFA500"),
|
||||
darkFg = Color.parseColor("#FFFFFF"),
|
||||
darkBg = Color.parseColor("#523B19"),
|
||||
darkLink = Color.parseColor("#FFBF00")
|
||||
)
|
||||
|
||||
val galaxyTheme = ReaderTheme(
|
||||
name = "Galaxy",
|
||||
lightFg = Color.parseColor("#000000"),
|
||||
lightBg = Color.parseColor("#F2F2F2"),
|
||||
lightLink = Color.parseColor("#800080"),
|
||||
darkFg = Color.parseColor("#FFFFFF"),
|
||||
darkBg = Color.parseColor("#000000"),
|
||||
darkLink = Color.parseColor("#B300B3")
|
||||
)
|
||||
|
||||
themes.addAll(listOf(forestTheme, oceanTheme, sunsetTheme, desertTheme, galaxyTheme))
|
||||
}
|
||||
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
checkNotch()
|
||||
super.onAttachedToWindow()
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityNovelReaderBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
settings = loadData("novel_reader_settings", this)
|
||||
?: NovelReaderSettings().apply { saveData("novel_reader_settings", this) }
|
||||
uiSettings = loadData("ui_settings", this)
|
||||
?: UserInterfaceSettings().also { saveData("ui_settings", it) }
|
||||
|
||||
controllerDuration = (uiSettings.animationSpeed * 200).toLong()
|
||||
|
||||
setupViews()
|
||||
setupBackPressedHandler()
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun setupViews() {
|
||||
scope.launch { binding.bookReader.openBook(intent.data!!) }
|
||||
binding.bookReader.setEbookReaderListener(this)
|
||||
|
||||
binding.novelReaderBack.setOnClickListener { finish() }
|
||||
binding.novelReaderSettings.setSafeOnClickListener {
|
||||
NovelReaderSettingsDialogFragment.newInstance().show(supportFragmentManager, NovelReaderSettingsDialogFragment.TAG)
|
||||
}
|
||||
|
||||
val gestureDetector = GestureDetectorCompat(this, object : GesturesListener() {
|
||||
override fun onSingleClick(event: MotionEvent) {
|
||||
handleController()
|
||||
}
|
||||
})
|
||||
|
||||
binding.bookReader.setOnTouchListener { _, event ->
|
||||
if (event != null) tryWith { gestureDetector.onTouchEvent(event) } ?: false
|
||||
else false
|
||||
}
|
||||
|
||||
binding.novelReaderNextChap.setOnClickListener { binding.novelReaderNextChapter.performClick() }
|
||||
binding.novelReaderNextChapter.setOnClickListener { binding.bookReader.next() }
|
||||
binding.novelReaderPrevChap.setOnClickListener { binding.novelReaderPreviousChapter.performClick() }
|
||||
binding.novelReaderPreviousChapter.setOnClickListener { binding.bookReader.prev() }
|
||||
|
||||
binding.novelReaderSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
binding.bookReader.gotoFraction(slider.value.toDouble())
|
||||
}
|
||||
})
|
||||
|
||||
onVolumeUp = { binding.novelReaderNextChapter.performClick() }
|
||||
|
||||
onVolumeDown = { binding.novelReaderPreviousChapter.performClick() }
|
||||
}
|
||||
|
||||
private fun setupBackPressedHandler() {
|
||||
var lastBackPressedTime: Long = 0
|
||||
val doublePressInterval: Long = 2000
|
||||
|
||||
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (binding.bookReader.canGoBack()) {
|
||||
binding.bookReader.goBack()
|
||||
} else {
|
||||
if (lastBackPressedTime + doublePressInterval > System.currentTimeMillis()) {
|
||||
finish()
|
||||
} else {
|
||||
snackString("Press back again to exit")
|
||||
lastBackPressedTime = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
override fun onBookLoadFailed(error: ReaderError) {
|
||||
snackString(error.message)
|
||||
finish()
|
||||
}
|
||||
|
||||
|
||||
override fun onBookLoaded(book: Book) {
|
||||
this.book = book
|
||||
val bookId = book.identifier!!
|
||||
toc = book.toc
|
||||
|
||||
val illegalCharsRegex = Regex("[^a-zA-Z0-9._-]")
|
||||
sanitizedBookId = bookId.replace(illegalCharsRegex, "_")
|
||||
|
||||
binding.novelReaderTitle.text = book.title
|
||||
binding.novelReaderSource.text = book.author?.joinToString(", ")
|
||||
|
||||
val tocLabels = book.toc.map { it.label ?: "" }
|
||||
binding.novelReaderChapterSelect.adapter = NoPaddingArrayAdapter(this, R.layout.item_dropdown, tocLabels)
|
||||
binding.novelReaderChapterSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
binding.bookReader.goto(book.toc[position].href)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
binding.bookReader.getAppearance {
|
||||
currentTheme = it
|
||||
themes.add(0, it)
|
||||
settings.default = loadData("${sanitizedBookId}_current_settings") ?: settings.default
|
||||
applySettings()
|
||||
}
|
||||
|
||||
val cfi = loadData<String>("${sanitizedBookId}_progress")
|
||||
|
||||
cfi?.let { binding.bookReader.goto(it) }
|
||||
binding.progress.visibility = View.GONE
|
||||
loaded = true
|
||||
}
|
||||
|
||||
|
||||
override fun onProgressChanged(info: RelocationInfo) {
|
||||
currentCfi = info.cfi
|
||||
binding.novelReaderSlider.value = info.fraction.toFloat()
|
||||
val pos = info.tocItem?.let { item -> toc.indexOfFirst { it == item } }
|
||||
if (pos != null) binding.novelReaderChapterSelect.setSelection(pos)
|
||||
saveData("${sanitizedBookId}_progress", info.cfi)
|
||||
}
|
||||
|
||||
|
||||
override fun onImageSelected(base64String: String) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val base64Data = base64String.substringAfter(",")
|
||||
val imageBytes: ByteArray = Base64.decode(base64Data, Base64.DEFAULT)
|
||||
val imageFile = File(cacheDir, "/images/ln.jpg")
|
||||
|
||||
imageFile.parentFile?.mkdirs()
|
||||
imageFile.createNewFile()
|
||||
|
||||
FileOutputStream(imageFile).use { outputStream -> outputStream.write(imageBytes) }
|
||||
|
||||
ImageViewDialog.newInstance(
|
||||
this@NovelReaderActivity,
|
||||
book.title,
|
||||
imageFile.toUri().toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onTextSelectionModeChange(mode: Boolean) {
|
||||
// TODO: Show ui for adding annotations and notes
|
||||
}
|
||||
|
||||
|
||||
private var onVolumeUp: (() -> Unit)? = null
|
||||
private var onVolumeDown: (() -> Unit)? = null
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
return when (event.keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_PAGE_UP -> {
|
||||
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP)
|
||||
if (!settings.default.volumeButtons)
|
||||
return false
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
onVolumeUp?.invoke()
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_PAGE_DOWN -> {
|
||||
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
|
||||
if (!settings.default.volumeButtons)
|
||||
return false
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
onVolumeDown?.invoke()
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
||||
else -> {
|
||||
super.dispatchKeyEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun applySettings() {
|
||||
saveData("${sanitizedBookId}_current_settings", settings.default)
|
||||
hideBars()
|
||||
|
||||
currentTheme = themes.first { it.name.equals(settings.default.currentThemeName, ignoreCase = true) }
|
||||
|
||||
when (settings.default.layout) {
|
||||
CurrentNovelReaderSettings.Layouts.PAGED -> {
|
||||
currentTheme?.flow = ReaderFlow.PAGINATED
|
||||
}
|
||||
|
||||
CurrentNovelReaderSettings.Layouts.SCROLLED -> {
|
||||
currentTheme?.flow = ReaderFlow.SCROLLED
|
||||
}
|
||||
}
|
||||
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
|
||||
when (settings.default.dualPageMode) {
|
||||
CurrentReaderSettings.DualPageModes.No -> currentTheme?.maxColumnCount = 1
|
||||
CurrentReaderSettings.DualPageModes.Automatic -> currentTheme?.maxColumnCount = 2
|
||||
CurrentReaderSettings.DualPageModes.Force -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
|
||||
currentTheme?.lineHeight = settings.default.lineHeight
|
||||
currentTheme?.gap = settings.default.margin
|
||||
currentTheme?.maxInlineSize = settings.default.maxInlineSize
|
||||
currentTheme?.maxBlockSize = settings.default.maxBlockSize
|
||||
currentTheme?.useDark = settings.default.useDarkTheme
|
||||
|
||||
currentTheme?.let { binding.bookReader.setAppearance(it) }
|
||||
|
||||
if (settings.default.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
|
||||
// region Handle Controls
|
||||
private var isContVisible = false
|
||||
private var isAnimating = false
|
||||
private var goneTimer = Timer()
|
||||
private var controllerDuration by Delegates.notNull<Long>()
|
||||
private val overshoot = OvershootInterpolator(1.4f)
|
||||
|
||||
fun gone() {
|
||||
goneTimer.cancel()
|
||||
goneTimer.purge()
|
||||
val timerTask: TimerTask = object : TimerTask() {
|
||||
override fun run() {
|
||||
if (!isContVisible) binding.novelReaderCont.post {
|
||||
binding.novelReaderCont.visibility = View.GONE
|
||||
isAnimating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
goneTimer = Timer()
|
||||
goneTimer.schedule(timerTask, controllerDuration)
|
||||
}
|
||||
|
||||
fun handleController(shouldShow: Boolean? = null) {
|
||||
if (!loaded) return
|
||||
|
||||
if (!settings.showSystemBars) {
|
||||
hideBars()
|
||||
applyNotchMargin()
|
||||
}
|
||||
|
||||
shouldShow?.apply { isContVisible = !this }
|
||||
if (isContVisible) {
|
||||
isContVisible = false
|
||||
if (!isAnimating) {
|
||||
isAnimating = true
|
||||
ObjectAnimator.ofFloat(binding.novelReaderCont, "alpha", 1f, 0f).setDuration(controllerDuration).start()
|
||||
ObjectAnimator.ofFloat(binding.novelReaderBottomCont, "translationY", 0f, 128f)
|
||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||
ObjectAnimator.ofFloat(binding.novelReaderTopLayout, "translationY", 0f, -128f)
|
||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||
}
|
||||
gone()
|
||||
} else {
|
||||
isContVisible = true
|
||||
binding.novelReaderCont.visibility = View.VISIBLE
|
||||
ObjectAnimator.ofFloat(binding.novelReaderCont, "alpha", 0f, 1f).setDuration(controllerDuration).start()
|
||||
ObjectAnimator.ofFloat(binding.novelReaderTopLayout, "translationY", -128f, 0f)
|
||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||
ObjectAnimator.ofFloat(binding.novelReaderBottomCont, "translationY", 128f, 0f)
|
||||
.apply { interpolator = overshoot;duration = controllerDuration;start() }
|
||||
}
|
||||
}
|
||||
// endregion Handle Controls
|
||||
|
||||
|
||||
private fun checkNotch() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !settings.showSystemBars) {
|
||||
val displayCutout = window.decorView.rootWindowInsets.displayCutout
|
||||
if (displayCutout != null) {
|
||||
if (displayCutout.boundingRects.size > 0) {
|
||||
notchHeight = min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height())
|
||||
applyNotchMargin()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun applyNotchMargin() {
|
||||
binding.novelReaderTopLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = notchHeight ?: return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun hideBars() {
|
||||
if (!settings.showSystemBars) hideSystemBars()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
package ani.dantotsu.media.novel.novelreader
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.NoPaddingArrayAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.BottomSheetCurrentNovelReaderSettingsBinding
|
||||
import ani.dantotsu.settings.CurrentNovelReaderSettings
|
||||
import ani.dantotsu.settings.CurrentReaderSettings
|
||||
|
||||
class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetCurrentNovelReaderSettingsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = BottomSheetCurrentNovelReaderSettingsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val activity = requireActivity() as NovelReaderActivity
|
||||
val settings = activity.settings.default
|
||||
|
||||
val themeLabels = activity.themes.map { it.name }
|
||||
binding.themeSelect.adapter = NoPaddingArrayAdapter(activity, R.layout.item_dropdown, themeLabels)
|
||||
binding.themeSelect.setSelection(themeLabels.indexOfFirst { it == settings.currentThemeName })
|
||||
binding.themeSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
settings.currentThemeName = themeLabels[position]
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
val layoutList = listOf(
|
||||
binding.paged,
|
||||
binding.continuous
|
||||
)
|
||||
|
||||
binding.layoutText.text = settings.layout.string
|
||||
var selected = layoutList[settings.layout.ordinal]
|
||||
selected.alpha = 1f
|
||||
|
||||
layoutList.forEachIndexed { index, imageButton ->
|
||||
imageButton.setOnClickListener {
|
||||
selected.alpha = 0.33f
|
||||
selected = imageButton
|
||||
selected.alpha = 1f
|
||||
settings.layout = CurrentNovelReaderSettings.Layouts[index]?:CurrentNovelReaderSettings.Layouts.PAGED
|
||||
binding.layoutText.text = settings.layout.string
|
||||
activity.applySettings()
|
||||
}
|
||||
}
|
||||
|
||||
val dualList = listOf(
|
||||
binding.dualNo,
|
||||
binding.dualAuto,
|
||||
binding.dualForce
|
||||
)
|
||||
|
||||
binding.dualPageText.text = settings.dualPageMode.toString()
|
||||
var selectedDual = dualList[settings.dualPageMode.ordinal]
|
||||
selectedDual.alpha = 1f
|
||||
|
||||
dualList.forEachIndexed { index, imageButton ->
|
||||
imageButton.setOnClickListener {
|
||||
selectedDual.alpha = 0.33f
|
||||
selectedDual = imageButton
|
||||
selectedDual.alpha = 1f
|
||||
settings.dualPageMode = CurrentReaderSettings.DualPageModes[index] ?: CurrentReaderSettings.DualPageModes.Automatic
|
||||
binding.dualPageText.text = settings.dualPageMode.toString()
|
||||
activity.applySettings()
|
||||
}
|
||||
}
|
||||
|
||||
binding.lineHeight.setText(settings.lineHeight.toString())
|
||||
binding.lineHeight.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
val value = binding.lineHeight.text.toString().toFloatOrNull() ?: 1.4f
|
||||
settings.lineHeight = value
|
||||
binding.lineHeight.setText(value.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
}
|
||||
|
||||
binding.incrementLineHeight.setOnClickListener {
|
||||
val value = binding.lineHeight.text.toString().toFloatOrNull() ?: 1.4f
|
||||
settings.lineHeight = value + 0.1f
|
||||
binding.lineHeight.setText(settings.lineHeight.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.decrementLineHeight.setOnClickListener {
|
||||
val value = binding.lineHeight.text.toString().toFloatOrNull() ?: 1.4f
|
||||
settings.lineHeight = value - 0.1f
|
||||
binding.lineHeight.setText(settings.lineHeight.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.margin.setText(settings.margin.toString())
|
||||
binding.margin.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
val value = binding.margin.text.toString().toFloatOrNull() ?: 0.06f
|
||||
settings.margin = value
|
||||
binding.margin.setText(value.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
}
|
||||
|
||||
binding.incrementMargin.setOnClickListener {
|
||||
val value = binding.margin.text.toString().toFloatOrNull() ?: 0.06f
|
||||
settings.margin = value + 0.01f
|
||||
binding.margin.setText(settings.margin.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.decrementMargin.setOnClickListener {
|
||||
val value = binding.margin.text.toString().toFloatOrNull() ?: 0.06f
|
||||
settings.margin = value - 0.01f
|
||||
binding.margin.setText(settings.margin.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.maxInlineSize.setText(settings.maxInlineSize.toString())
|
||||
binding.maxInlineSize.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
val value = binding.maxInlineSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.maxInlineSize = value
|
||||
binding.maxInlineSize.setText(value.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
}
|
||||
|
||||
binding.incrementMaxInlineSize.setOnClickListener {
|
||||
val value = binding.maxInlineSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.maxInlineSize = value + 10
|
||||
binding.maxInlineSize.setText(settings.maxInlineSize.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.decrementMaxInlineSize.setOnClickListener {
|
||||
val value = binding.maxInlineSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.maxInlineSize = value - 10
|
||||
binding.maxInlineSize.setText(settings.maxInlineSize.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.maxBlockSize.setText(settings.maxBlockSize.toString())
|
||||
binding.maxBlockSize.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
val value = binding.maxBlockSize.text.toString().toIntOrNull() ?: 720
|
||||
settings.maxBlockSize = value
|
||||
binding.maxBlockSize.setText(value.toString())
|
||||
activity.applySettings()
|
||||
}
|
||||
}
|
||||
|
||||
binding.useDarkTheme.isChecked = settings.useDarkTheme
|
||||
binding.useDarkTheme.setOnCheckedChangeListener { _,isChecked ->
|
||||
settings.useDarkTheme = isChecked
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.keepScreenOn.isChecked = settings.keepScreenOn
|
||||
binding.keepScreenOn.setOnCheckedChangeListener { _,isChecked ->
|
||||
settings.keepScreenOn = isChecked
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.volumeButton.isChecked = settings.volumeButtons
|
||||
binding.volumeButton.setOnCheckedChangeListener { _,isChecked ->
|
||||
settings.volumeButtons = isChecked
|
||||
activity.applySettings()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
companion object{
|
||||
fun newInstance() = NovelReaderSettingsDialogFragment()
|
||||
const val TAG = "NovelReaderSettingsDialogFragment"
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue