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