Initial commit

This commit is contained in:
Finnley Somdahl 2023-10-17 18:42:43 -05:00
commit 21bfbfb139
520 changed files with 47819 additions and 0 deletions

View 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
}
}
}
}

View file

@ -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)
}

View 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()
}
}

View file

@ -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)
}
}

View 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
}
}
}
}

View file

@ -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()
}
}

View file

@ -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"
}
}