Information bar in reader
This commit is contained in:
@@ -11,12 +11,6 @@ import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||
@@ -25,6 +19,12 @@ import org.koitharu.kotatsu.utils.ext.getEnumValue
|
||||
import org.koitharu.kotatsu.utils.ext.observe
|
||||
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
||||
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
@@ -203,6 +203,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isSuggestionsExcludeNsfw: Boolean
|
||||
get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false)
|
||||
|
||||
val isReaderBarEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_BAR, true)
|
||||
|
||||
val dnsOverHttps: DoHProvider
|
||||
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
||||
|
||||
@@ -320,6 +323,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_EXIT_CONFIRM = "exit_confirm"
|
||||
const val KEY_INCOGNITO_MODE = "incognito"
|
||||
const val KEY_SYNC = "sync"
|
||||
const val KEY_READER_BAR = "reader_bar"
|
||||
|
||||
// About
|
||||
const val KEY_APP_UPDATE = "app_update"
|
||||
|
||||
@@ -21,7 +21,7 @@ fun <T> AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() ->
|
||||
fun <T> AppSettings.observeAsLiveData(
|
||||
context: CoroutineContext,
|
||||
key: String,
|
||||
valueProducer: AppSettings.() -> T
|
||||
valueProducer: AppSettings.() -> T,
|
||||
) = liveData(context) {
|
||||
emit(valueProducer())
|
||||
observe().collect {
|
||||
@@ -32,4 +32,4 @@ fun <T> AppSettings.observeAsLiveData(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StyleRes
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.utils.ext.getAnimationDuration
|
||||
|
||||
class ReadingProgressView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@@ -20,7 +21,7 @@ class ReadingProgressView @JvmOverloads constructor(
|
||||
) : View(context, attrs, defStyleAttr), ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener {
|
||||
|
||||
private var percentAnimator: ValueAnimator? = null
|
||||
private val animationDuration = context.resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
|
||||
private val animationDuration = context.getAnimationDuration(android.R.integer.config_shortAnimTime)
|
||||
|
||||
@StyleRes
|
||||
private val drawableStyle: Int
|
||||
@@ -76,7 +77,7 @@ class ReadingProgressView @JvmOverloads constructor(
|
||||
percentAnimator?.cancel()
|
||||
percentAnimator = ValueAnimator.ofFloat(
|
||||
currentDrawable.progress.coerceAtLeast(0f),
|
||||
value
|
||||
value,
|
||||
).apply {
|
||||
duration = animationDuration
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
@@ -111,4 +112,4 @@ class ReadingProgressView @JvmOverloads constructor(
|
||||
outline.setOval(0, 0, view.width, view.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,14 @@ import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.transition.Fade
|
||||
import android.transition.Slide
|
||||
import android.transition.TransitionManager
|
||||
import android.transition.TransitionSet
|
||||
import android.view.*
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.view.*
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.transition.Slide
|
||||
import androidx.transition.TransitionManager
|
||||
import androidx.transition.TransitionSet
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -101,6 +99,7 @@ class ReaderActivity :
|
||||
onLoadingStateChanged(viewModel.isLoading.value == true)
|
||||
}
|
||||
viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure)
|
||||
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
|
||||
viewModel.isBookmarkAdded.observe(this, this::onBookmarkStateChanged)
|
||||
viewModel.onShowToast.observe(this) { msgId ->
|
||||
Snackbar.make(binding.container, msgId, Snackbar.LENGTH_SHORT)
|
||||
@@ -280,12 +279,14 @@ class ReaderActivity :
|
||||
val transition = TransitionSet()
|
||||
.setOrdering(TransitionSet.ORDERING_TOGETHER)
|
||||
.addTransition(Slide(Gravity.TOP).addTarget(binding.appbarTop))
|
||||
.addTransition(Fade().addTarget(binding.infoBar))
|
||||
binding.appbarBottom?.let { bottomBar ->
|
||||
transition.addTransition(Slide(Gravity.BOTTOM).addTarget(bottomBar))
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(binding.root, transition)
|
||||
binding.appbarTop.isVisible = isUiVisible
|
||||
binding.appbarBottom?.isVisible = isUiVisible
|
||||
binding.infoBar.isGone = isUiVisible || (viewModel.isInfoBarEnabled.value == false)
|
||||
if (isUiVisible) {
|
||||
showSystemUI()
|
||||
} else {
|
||||
@@ -322,6 +323,10 @@ class ReaderActivity :
|
||||
setUiIsVisible(!binding.appbarTop.isVisible)
|
||||
}
|
||||
|
||||
private fun onReaderBarChanged(isBarEnabled: Boolean) {
|
||||
binding.infoBar.isVisible = isBarEnabled && binding.appbarTop.isGone
|
||||
}
|
||||
|
||||
private fun onBookmarkStateChanged(isAdded: Boolean) {
|
||||
val menuItem = binding.toolbarBottom.menu.findItem(R.id.action_bookmark) ?: return
|
||||
menuItem.setTitle(if (isAdded) R.string.bookmark_remove else R.string.bookmark_add)
|
||||
@@ -330,6 +335,7 @@ class ReaderActivity :
|
||||
|
||||
private fun onUiStateChanged(uiState: ReaderUiState?, previous: ReaderUiState?) {
|
||||
title = uiState?.chapterName ?: uiState?.mangaName ?: getString(R.string.loading_)
|
||||
binding.infoBar.update(uiState)
|
||||
if (uiState == null) {
|
||||
supportActionBar?.subtitle = null
|
||||
binding.slider.isVisible = false
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import com.google.android.material.R as materialR
|
||||
import java.util.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.parsers.util.format
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||
|
||||
class ReaderInfoBarView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val textBounds = Rect()
|
||||
private val inset = context.resources.resolveDp(2f)
|
||||
private val timeFormat = SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT)
|
||||
private val timeReceiver = TimeReceiver()
|
||||
|
||||
private var timeText = timeFormat.format(Date())
|
||||
private var text: String = ""
|
||||
|
||||
private val innerHeight
|
||||
get() = height - inset - inset - paddingTop - paddingBottom
|
||||
|
||||
private val innerWidth
|
||||
get() = width - inset - inset - paddingLeft - paddingRight
|
||||
|
||||
init {
|
||||
paint.color = ColorUtils.setAlphaComponent(
|
||||
context.getThemeColor(materialR.attr.colorOnSurface, Color.BLACK),
|
||||
160,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val ty = innerHeight / 2f + textBounds.height() / 2f - textBounds.bottom
|
||||
paint.textAlign = Paint.Align.LEFT
|
||||
canvas.drawText(text, paddingLeft + inset, paddingTop + inset + ty, paint)
|
||||
paint.textAlign = Paint.Align.RIGHT
|
||||
canvas.drawText(timeText, width - paddingRight - inset, paddingTop + inset + ty, paint)
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
updateTextSize()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
context.registerReceiver(timeReceiver, IntentFilter(Intent.ACTION_TIME_TICK))
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
context.unregisterReceiver(timeReceiver)
|
||||
}
|
||||
|
||||
fun update(state: ReaderUiState?) {
|
||||
text = if (state != null) {
|
||||
val percent = state.computePercent()
|
||||
context.getString(
|
||||
R.string.reader_info_pattern,
|
||||
state.chapterNumber,
|
||||
state.chaptersTotal,
|
||||
state.currentPage + 1,
|
||||
state.totalPages,
|
||||
) + if (percent in 0f..1f) {
|
||||
" " + context.getString(R.string.percent_string_pattern, (percent * 100).format())
|
||||
} else {
|
||||
""
|
||||
}
|
||||
} else {
|
||||
""
|
||||
}
|
||||
updateTextSize()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun updateTextSize() {
|
||||
val str = text + timeText
|
||||
val testTextSize = 48f
|
||||
paint.textSize = testTextSize
|
||||
paint.getTextBounds(str, 0, str.length, textBounds)
|
||||
paint.textSize = testTextSize * innerHeight / textBounds.height()
|
||||
paint.getTextBounds(str, 0, str.length, textBounds)
|
||||
}
|
||||
|
||||
private inner class TimeReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
timeText = timeFormat.format(Date())
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,12 @@ class ReaderViewModel @AssistedInject constructor(
|
||||
valueProducer = { readerAnimation },
|
||||
)
|
||||
|
||||
val isInfoBarEnabled = settings.observeAsLiveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
key = AppSettings.KEY_READER_BAR,
|
||||
valueProducer = { isReaderBarEnabled },
|
||||
)
|
||||
|
||||
val isScreenshotsBlockEnabled = combine(
|
||||
mangaData,
|
||||
settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy },
|
||||
|
||||
@@ -7,4 +7,12 @@ data class ReaderUiState(
|
||||
val chaptersTotal: Int,
|
||||
val currentPage: Int,
|
||||
val totalPages: Int,
|
||||
)
|
||||
) {
|
||||
|
||||
fun computePercent(): Float {
|
||||
val ppc = 1f / chaptersTotal
|
||||
val chapterIndex = chapterNumber - 1
|
||||
val pagePercent = (currentPage + 1) / totalPages.toFloat()
|
||||
return ppc * chapterIndex + ppc * pagePercent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,15 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<org.koitharu.kotatsu.reader.ui.ReaderInfoBarView
|
||||
android:id="@+id/infoBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp"
|
||||
android:layout_gravity="top"
|
||||
android:paddingHorizontal="6dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.koitharu.kotatsu.reader.ui.ReaderToastView
|
||||
android:id="@+id/toastView"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -25,6 +25,15 @@
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark"
|
||||
tools:text="@string/loading_" />
|
||||
|
||||
<org.koitharu.kotatsu.reader.ui.ReaderInfoBarView
|
||||
android:id="@+id/infoBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp"
|
||||
android:layout_gravity="top"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_top"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -364,4 +364,6 @@
|
||||
<string name="automatic_scroll">Automatic scroll</string>
|
||||
<string name="off_short">Off</string>
|
||||
<string name="seconds_pattern">%ss</string>
|
||||
<string name="reader_info_pattern">Ch. %1$d/%2$d Pg. %3$d/%4$d</string>
|
||||
<string name="reader_info_bar">Show information bar in reader</string>
|
||||
</resources>
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
android:key="reader_animation"
|
||||
android:title="@string/pages_animation" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="reader_bar"
|
||||
android:title="@string/reader_info_bar" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="pages_numbers"
|
||||
@@ -45,7 +50,6 @@
|
||||
android:key="screenshots_policy"
|
||||
android:title="@string/screenshots_policy"
|
||||
app:defaultValue="allow"
|
||||
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
@@ -57,4 +61,4 @@
|
||||
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user