增加书籍详情界面与有声书界面的根据封面生成主题色功能
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
package io.legato.kazusa.ui.book.audio
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.TransitionDrawable
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -11,10 +18,15 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.color.DynamicColorsOptions
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.transition.platform.MaterialContainerTransform
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.VMBaseActivity
|
||||
import io.legato.kazusa.constant.BookType
|
||||
@@ -50,7 +62,10 @@ import io.legato.kazusa.utils.startActivityForBook
|
||||
import io.legato.kazusa.utils.startAnimation
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
import io.legato.kazusa.utils.visible
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import splitties.views.onLongClick
|
||||
@@ -70,6 +85,13 @@ class AudioPlayActivity :
|
||||
private var adjustProgress = false
|
||||
private var playMode = AudioPlay.PlayMode.LIST_END_STOP
|
||||
private var playSpeed = 1f
|
||||
private var primaryFinalColor: Int = 0
|
||||
private var surfaceFinalColor: Int = 0
|
||||
private var secondaryFinalColor: Int = 0
|
||||
private var onSurfaceFinalColor: Int = 0
|
||||
private var secondaryContainerFinalColor: Int = 0
|
||||
private var currentJob: Job? = null
|
||||
private var wrappedContext: Context? = null
|
||||
|
||||
private val progressTimeFormat by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
@@ -95,7 +117,27 @@ class AudioPlayActivity :
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val transform = MaterialContainerTransform().apply {
|
||||
addTarget(binding.root)
|
||||
scrimColor = Color.TRANSPARENT
|
||||
}
|
||||
window.sharedElementEnterTransition = transform
|
||||
window.sharedElementReturnTransition = transform
|
||||
super.onCreate(savedInstanceState)
|
||||
binding.root.transitionName = intent.getStringExtra("transitionName")
|
||||
primaryFinalColor =
|
||||
MaterialColors.getColor(this, androidx.appcompat.R.attr.colorPrimary, -1)
|
||||
surfaceFinalColor =
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface, -1)
|
||||
secondaryFinalColor =
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondary, -1)
|
||||
onSurfaceFinalColor =
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface, -1)
|
||||
secondaryContainerFinalColor = MaterialColors.getColor(
|
||||
this,
|
||||
com.google.android.material.R.attr.colorSecondaryContainer,
|
||||
-1
|
||||
)
|
||||
binding.titleBar.setBackgroundResource(R.color.transparent)
|
||||
binding.titleBar.title = ""
|
||||
AudioPlay.register(this)
|
||||
@@ -318,25 +360,29 @@ class AudioPlayActivity :
|
||||
}
|
||||
|
||||
private fun upCover(path: String?) {
|
||||
BookCover.load(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl) {
|
||||
if (!AppConfig.isEInkMode) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
BookCover.load(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl)
|
||||
.into(binding.ivBg)
|
||||
val blurEffect =
|
||||
RenderEffect.createBlurEffect(120f, 120f, Shader.TileMode.CLAMP)
|
||||
binding.ivBg.setRenderEffect(blurEffect)
|
||||
} else {
|
||||
// 低版本直接加载模糊图
|
||||
BookCover.loadBlur(
|
||||
this,
|
||||
path,
|
||||
sourceOrigin = AudioPlay.bookSource?.bookSourceUrl
|
||||
)
|
||||
.into(binding.ivBg)
|
||||
BookCover.load(
|
||||
this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl,
|
||||
onLoadFinish = {
|
||||
binding.ivCover.post {
|
||||
val drawable = binding.ivCover.drawable
|
||||
if (drawable != null) {
|
||||
addColorScheme(drawable)
|
||||
}
|
||||
}
|
||||
}).into(binding.ivCover)
|
||||
if (!AppConfig.isEInkMode) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
BookCover.load(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl)
|
||||
.into(binding.ivBg)
|
||||
val blurEffect = RenderEffect.createBlurEffect(120f, 120f, Shader.TileMode.CLAMP)
|
||||
binding.ivBg.setRenderEffect(blurEffect)
|
||||
} else {
|
||||
// 低版本直接加载模糊图
|
||||
BookCover.loadBlur(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl)
|
||||
.into(binding.ivBg)
|
||||
}
|
||||
}.into(binding.ivCover)
|
||||
}
|
||||
addColorScheme(binding.ivCover.drawable)
|
||||
}
|
||||
|
||||
private fun playButton() {
|
||||
@@ -347,6 +393,145 @@ class AudioPlayActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun addColorScheme(drawable: Drawable?) {
|
||||
currentJob?.cancel()
|
||||
currentJob = CoroutineScope(Dispatchers.Default).launch {
|
||||
val bitmap = when (drawable) {
|
||||
is BitmapDrawable -> drawable.bitmap
|
||||
is TransitionDrawable -> (drawable.getDrawable(1) as? BitmapDrawable)?.bitmap
|
||||
else -> null
|
||||
} ?: return@launch
|
||||
|
||||
val colorAccuracy = true
|
||||
val targetWidth = if (colorAccuracy) (bitmap.width / 4).coerceAtMost(256) else 16
|
||||
val targetHeight = if (colorAccuracy) (bitmap.height / 4).coerceAtMost(256) else 16
|
||||
val scaledBitmap = bitmap.scale(targetWidth, targetHeight, false)
|
||||
|
||||
val options = DynamicColorsOptions.Builder()
|
||||
.setContentBasedSource(scaledBitmap)
|
||||
.build()
|
||||
|
||||
wrappedContext = DynamicColors.wrapContextIfAvailable(
|
||||
this@AudioPlayActivity,
|
||||
options
|
||||
).apply {
|
||||
resources.configuration.uiMode =
|
||||
this@AudioPlayActivity.resources.configuration.uiMode
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
applyColorScheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun applyColorScheme() {
|
||||
val ctx = wrappedContext ?: this
|
||||
|
||||
val colorPrimary = MaterialColors.getColor(ctx, androidx.appcompat.R.attr.colorPrimary, -1)
|
||||
val colorSecondary =
|
||||
MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSecondary, -1)
|
||||
val colorOnSurface =
|
||||
MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorOnSurface, -1)
|
||||
val colorSurface =
|
||||
MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSurface, -1)
|
||||
val colorSurfaceContainer = MaterialColors.getColor(
|
||||
ctx,
|
||||
com.google.android.material.R.attr.colorSurfaceContainer,
|
||||
-1
|
||||
)
|
||||
val colorSecondaryContainer = MaterialColors.getColor(
|
||||
ctx,
|
||||
com.google.android.material.R.attr.colorSecondaryContainer,
|
||||
-1
|
||||
)
|
||||
|
||||
val primaryTransition = ValueAnimator.ofArgb(primaryFinalColor, colorPrimary).apply {
|
||||
duration = 400L
|
||||
addUpdateListener { animation ->
|
||||
val color = animation.animatedValue as Int
|
||||
binding.settingSlider.thumbTintList = ColorStateList.valueOf(color)
|
||||
binding.settingSlider.trackActiveTintList = ColorStateList.valueOf(color)
|
||||
binding.playerProgress.thumbTintList = ColorStateList.valueOf(color)
|
||||
binding.playerProgress.trackActiveTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
}
|
||||
|
||||
val secondaryContainerTransition =
|
||||
ValueAnimator.ofArgb(secondaryContainerFinalColor, colorSecondaryContainer).apply {
|
||||
duration = 400L
|
||||
addUpdateListener { animation ->
|
||||
val color = animation.animatedValue as Int
|
||||
binding.fabPlayStop.backgroundTintList = ColorStateList.valueOf(color)
|
||||
binding.btnReset.backgroundTintList = ColorStateList.valueOf(color)
|
||||
binding.settingSlider.trackInactiveTintList = ColorStateList.valueOf(color)
|
||||
binding.playerProgress.trackInactiveTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
}
|
||||
|
||||
val surfaceTransition =
|
||||
ValueAnimator.ofArgb(surfaceFinalColor, colorSurfaceContainer).apply {
|
||||
duration = 400L
|
||||
addUpdateListener { animation ->
|
||||
val color = animation.animatedValue as Int
|
||||
binding.vwBg2.setBackgroundColor(color)
|
||||
binding.cdTimer.setBackgroundColor(color)
|
||||
binding.cdSpeed.setBackgroundColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
val textTransition = ValueAnimator.ofArgb(onSurfaceFinalColor, colorOnSurface).apply {
|
||||
duration = 400L
|
||||
addUpdateListener { animation ->
|
||||
val color = animation.animatedValue as Int
|
||||
binding.tvSpeed.setTextColor(color)
|
||||
binding.tvTimer.setTextColor(color)
|
||||
binding.tvTitle.setTextColor(color)
|
||||
binding.tvDurTime.setTextColor(color)
|
||||
binding.tvAllTime.setTextColor(color)
|
||||
binding.tvSubTitle.setTextColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
surfaceTransition.start()
|
||||
textTransition.start()
|
||||
primaryTransition.start()
|
||||
secondaryContainerTransition.start()
|
||||
}
|
||||
|
||||
surfaceFinalColor = colorSurface
|
||||
secondaryFinalColor = colorSecondary
|
||||
onSurfaceFinalColor = colorOnSurface
|
||||
primaryFinalColor = colorPrimary
|
||||
secondaryContainerFinalColor = colorSecondaryContainer
|
||||
|
||||
binding.progressLoading.setIndicatorColor(colorPrimary)
|
||||
|
||||
val states = arrayOf(
|
||||
intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked),
|
||||
intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked),
|
||||
intArrayOf(-android.R.attr.state_enabled)
|
||||
)
|
||||
|
||||
val colors = intArrayOf(
|
||||
colorSurface,
|
||||
colorOnSurface,
|
||||
colorOnSurface.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
val stateList = ColorStateList(states, colors)
|
||||
listOf(
|
||||
binding.ivSkipNext, binding.ivSkipPrevious, binding.ivPlayMode, binding.ivTimer,
|
||||
binding.ivChapter, binding.ivFastForward, binding.btnReset, binding.fabPlayStop
|
||||
).forEach { btn ->
|
||||
btn.setTextColor(stateList)
|
||||
btn.iconTint = stateList
|
||||
}
|
||||
}
|
||||
|
||||
override val oldBook: Book?
|
||||
get() = AudioPlay.book
|
||||
|
||||
@@ -487,4 +672,10 @@ class AudioPlayActivity :
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.copy(alpha: Float): Int {
|
||||
val alpha = (alpha * 255).toInt()
|
||||
return (this and 0x00FFFFFF) or (alpha shl 24)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package io.legato.kazusa.ui.book.info
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.graphics.drawable.TransitionDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -15,8 +22,12 @@ import android.widget.LinearLayout
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.color.DynamicColorsOptions
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.transition.platform.MaterialContainerTransform
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.VMBaseActivity
|
||||
@@ -75,7 +86,10 @@ import io.legato.kazusa.utils.startActivity
|
||||
import io.legato.kazusa.utils.toastOnUi
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
import io.legato.kazusa.utils.visible
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -144,6 +158,13 @@ class BookInfoActivity :
|
||||
viewModel.refreshBook(book)
|
||||
}
|
||||
}
|
||||
|
||||
private var surfaceFinalColor: Int = 0
|
||||
private var secondaryFinalColor: Int = 0
|
||||
private var onSurfaceFinalColor: Int = 0
|
||||
private var secondaryContainerFinalColor: Int = 0
|
||||
private var currentJob: Job? = null
|
||||
private var wrappedContext: Context? = null
|
||||
private var chapterChanged = false
|
||||
private val waitDialog by lazy { WaitDialog(this) }
|
||||
private var editMenuItem: MenuItem? = null
|
||||
@@ -156,15 +177,26 @@ class BookInfoActivity :
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
postponeEnterTransition()
|
||||
val transform = MaterialContainerTransform().apply {
|
||||
addTarget(binding.ivCover)
|
||||
addTarget(binding.cdCov)
|
||||
scrimColor = Color.TRANSPARENT
|
||||
}
|
||||
window.sharedElementEnterTransition = transform
|
||||
window.sharedElementReturnTransition = transform
|
||||
window.sharedElementsUseOverlay = false
|
||||
super.onCreate(savedInstanceState)
|
||||
binding.ivCover.transitionName = intent.getStringExtra("transitionName")
|
||||
binding.ivCover.doOnPreDraw {
|
||||
surfaceFinalColor =
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface, -1)
|
||||
secondaryFinalColor =
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondary, -1)
|
||||
onSurfaceFinalColor =
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface, -1)
|
||||
secondaryContainerFinalColor = MaterialColors.getColor(
|
||||
this,
|
||||
com.google.android.material.R.attr.colorSecondaryContainer,
|
||||
-1
|
||||
)
|
||||
binding.cdCov.transitionName = intent.getStringExtra("transitionName")
|
||||
binding.cdCov.doOnPreDraw {
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
setSupportActionBar(binding.topBar)
|
||||
@@ -342,6 +374,7 @@ class BookInfoActivity :
|
||||
|
||||
private fun showBook(book: Book) = binding.run {
|
||||
showCover(book)
|
||||
addColorScheme(binding.ivCover.drawable)
|
||||
tvName.text = book.name
|
||||
tvAuthor.text = getString(R.string.author_show, book.getRealAuthor())
|
||||
tvOrigin.text = getString(R.string.origin_show, book.originName)
|
||||
@@ -377,7 +410,21 @@ class BookInfoActivity :
|
||||
}
|
||||
|
||||
private fun showCover(book: Book) {
|
||||
binding.ivCover.load(book.getDisplayCover(), book.name, book.author, false, book.origin) {
|
||||
binding.ivCover.load(
|
||||
book.getDisplayCover(),
|
||||
book.name,
|
||||
book.author,
|
||||
false,
|
||||
book.origin,
|
||||
onLoadFinish = {
|
||||
binding.ivCover.post {
|
||||
val drawable = binding.ivCover.drawable
|
||||
if (drawable != null) {
|
||||
addColorScheme(drawable)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
if (!AppConfig.isEInkMode) {
|
||||
//高版本使用RenderEffect
|
||||
BookCover.load(this, book.getDisplayCover(), false, book.origin)
|
||||
@@ -392,9 +439,139 @@ class BookInfoActivity :
|
||||
.into(binding.bgBook)
|
||||
}
|
||||
}
|
||||
addColorScheme(binding.ivCover.drawable)
|
||||
|
||||
}
|
||||
|
||||
private fun addColorScheme(drawable: Drawable?) {
|
||||
currentJob?.cancel()
|
||||
currentJob = CoroutineScope(Dispatchers.Default).launch {
|
||||
val bitmap = when (drawable) {
|
||||
is BitmapDrawable -> drawable.bitmap
|
||||
is TransitionDrawable -> (drawable.getDrawable(1) as? BitmapDrawable)?.bitmap
|
||||
else -> null
|
||||
} ?: return@launch
|
||||
|
||||
val colorAccuracy = true
|
||||
val targetWidth = if (colorAccuracy) (bitmap.width / 4).coerceAtMost(256) else 16
|
||||
val targetHeight = if (colorAccuracy) (bitmap.height / 4).coerceAtMost(256) else 16
|
||||
val scaledBitmap = bitmap.scale(targetWidth, targetHeight, false)
|
||||
|
||||
val options = DynamicColorsOptions.Builder()
|
||||
.setContentBasedSource(scaledBitmap)
|
||||
.build()
|
||||
|
||||
wrappedContext = DynamicColors.wrapContextIfAvailable(
|
||||
this@BookInfoActivity,
|
||||
options
|
||||
).apply {
|
||||
resources.configuration.uiMode =
|
||||
this@BookInfoActivity.resources.configuration.uiMode
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
applyColorScheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun applyColorScheme() {
|
||||
val ctx = wrappedContext ?: this
|
||||
|
||||
val colorPrimary = MaterialColors.getColor(ctx, androidx.appcompat.R.attr.colorPrimary, -1)
|
||||
val colorSecondary =
|
||||
MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSecondary, -1)
|
||||
val colorTertiary =
|
||||
MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorTertiary, -1)
|
||||
val colorOnSurface =
|
||||
MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorOnSurface, -1)
|
||||
val colorSurface =
|
||||
MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSurface, -1)
|
||||
val colorSurfaceContainer = MaterialColors.getColor(
|
||||
ctx,
|
||||
com.google.android.material.R.attr.colorSurfaceContainer,
|
||||
-1
|
||||
)
|
||||
val colorSecondaryContainer = MaterialColors.getColor(
|
||||
ctx,
|
||||
com.google.android.material.R.attr.colorSecondaryContainer,
|
||||
-1
|
||||
)
|
||||
|
||||
val surfaceTransition = ValueAnimator.ofArgb(surfaceFinalColor, colorSurface).apply {
|
||||
duration = 400L
|
||||
addUpdateListener { animation ->
|
||||
val color = animation.animatedValue as Int
|
||||
binding.cdInfo?.setCardBackgroundColor(color)
|
||||
binding.llInfo.setBackgroundColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
val buttonTransition = ValueAnimator.ofArgb(secondaryFinalColor, colorSecondary).apply {
|
||||
duration = 400L
|
||||
addUpdateListener { animation ->
|
||||
val color = animation.animatedValue as Int
|
||||
val tint = ColorStateList.valueOf(color)
|
||||
listOf(
|
||||
binding.btnShelf,
|
||||
binding.tvTocView,
|
||||
binding.tvChangeGroup,
|
||||
binding.btnChangeSource
|
||||
).forEach { btn ->
|
||||
btn.setTextColor(color)
|
||||
btn.iconTint = tint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val backTransition =
|
||||
ValueAnimator.ofArgb(secondaryContainerFinalColor, colorSecondaryContainer).apply {
|
||||
duration = 400L
|
||||
addUpdateListener { animation ->
|
||||
val color = animation.animatedValue as Int
|
||||
binding.bgBookMask.setBackgroundColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
val textTransition = ValueAnimator.ofArgb(onSurfaceFinalColor, colorOnSurface).apply {
|
||||
duration = 400L
|
||||
addUpdateListener { animation ->
|
||||
val color = animation.animatedValue as Int
|
||||
binding.tvName.setTextColor(color)
|
||||
binding.tvAuthor.setTextColor(color)
|
||||
binding.tvOrigin.setTextColor(color)
|
||||
binding.tvDetail.setTextColor(color)
|
||||
binding.ivName.imageTintList = ColorStateList.valueOf(color)
|
||||
binding.ivWeb.imageTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
surfaceTransition.start()
|
||||
buttonTransition.start()
|
||||
textTransition.start()
|
||||
backTransition.start()
|
||||
}
|
||||
|
||||
surfaceFinalColor = colorSurface
|
||||
secondaryFinalColor = colorSecondary
|
||||
onSurfaceFinalColor = colorOnSurface
|
||||
|
||||
(binding.llCover.background as? GradientDrawable)?.colors =
|
||||
intArrayOf(Color.TRANSPARENT, colorSurface)
|
||||
|
||||
binding.collTopBar.setContentScrimColor(colorSurfaceContainer)
|
||||
binding.lbKind.applyColorScheme(colorSurfaceContainer, colorOnSurface)
|
||||
binding.tvChapter.setTextColor(colorPrimary)
|
||||
binding.div.setTextColor(colorSecondary)
|
||||
binding.tvChapterIndex.setTextColor(colorSecondary)
|
||||
binding.btnRead.setBackgroundColor(colorTertiary)
|
||||
binding.tvToc.setTextColor(colorOnSurface)
|
||||
binding.tvLasted.setTextColor(colorOnSurface)
|
||||
}
|
||||
|
||||
private fun upLoading(isLoading: Boolean, chapterList: List<BookChapter>? = null) {
|
||||
when {
|
||||
isLoading -> {
|
||||
|
||||
@@ -47,9 +47,20 @@ class LabelsBar @JvmOverloads constructor(
|
||||
usedChips.add(it)
|
||||
}
|
||||
}
|
||||
style(chip) // 在这里应用外部传入的样式
|
||||
style(chip)
|
||||
chip.text = label
|
||||
addView(chip)
|
||||
}
|
||||
|
||||
fun applyColorScheme(colorPrimaryContainer: Int, colorOnPrimaryContainer: Int) {
|
||||
usedChips.forEach { chip ->
|
||||
chip.chipBackgroundColor = ColorStateList.valueOf(colorPrimaryContainer)
|
||||
chip.setTextColor(colorOnPrimaryContainer)
|
||||
}
|
||||
unUsedChips.forEach { chip ->
|
||||
chip.chipBackgroundColor = ColorStateList.valueOf(colorPrimaryContainer)
|
||||
chip.setTextColor(colorOnPrimaryContainer)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,9 +10,16 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/bg_image"
|
||||
android:foreground="@drawable/bg_book_mask_land"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bg_book_mask"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0.80"
|
||||
android:background="@drawable/bg_gradient_mask"
|
||||
android:contentDescription="@string/bg_image" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
@@ -21,6 +28,7 @@
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/coll_top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
style="?attr/collapsingToolbarLayoutLargeSize"
|
||||
@@ -58,302 +66,301 @@
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_cover"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_weight="3"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="32dp"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cd_cov"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginStart="32dp"
|
||||
app:cardCornerRadius="16dp">
|
||||
app:cardCornerRadius="8dp"
|
||||
app:layout_constraintDimensionRatio="5:7"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.3"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.5"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<io.legato.kazusa.ui.widget.image.CoverImageView
|
||||
android:id="@+id/iv_cover"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="210dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/img_cover"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/image_cover_default" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingHorizontal="30dp">
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
style="@style/TextAppearance.Material3.ActionBar.Title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/book_name"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/cd_cov"
|
||||
app:layout_constraintTop_toBottomOf="@id/cd_cov" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/book_name"
|
||||
style="@style/TextAppearance.Material3.ActionBar.Title"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
<ImageView
|
||||
android:id="@+id/iv_name"
|
||||
android:layout_width="18sp"
|
||||
android:layout_height="18sp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:contentDescription="@string/author"
|
||||
android:src="@drawable/ic_author"
|
||||
app:layout_constraintStart_toStartOf="@id/cd_cov"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_name"
|
||||
tools:ignore="RtlHardcoded,RtlSymmetry" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="2dp"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
<TextView
|
||||
android:id="@+id/tv_author"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:text="@string/author"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/iv_name"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/iv_name"
|
||||
app:layout_constraintTop_toTopOf="@id/iv_name"
|
||||
tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="18sp"
|
||||
android:layout_height="18sp"
|
||||
android:contentDescription="@string/author"
|
||||
android:src="@drawable/ic_author"
|
||||
tools:ignore="RtlHardcoded,RtlSymmetry" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_author"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="2dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:text="@string/author"
|
||||
android:textSize="14sp"
|
||||
tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry" />
|
||||
<ImageView
|
||||
android:id="@+id/iv_web"
|
||||
android:layout_width="18sp"
|
||||
android:layout_height="18sp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:contentDescription="@string/origin_format"
|
||||
android:src="@drawable/ic_web_outline"
|
||||
app:layout_constraintStart_toStartOf="@id/cd_cov"
|
||||
app:layout_constraintTop_toBottomOf="@id/iv_name"
|
||||
tools:ignore="RtlHardcoded,RtlSymmetry" />
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/tv_origin"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/iv_web"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/iv_web"
|
||||
app:layout_constraintTop_toTopOf="@id/iv_web"
|
||||
tools:text="@string/origin_format" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_web"
|
||||
android:layout_width="18sp"
|
||||
android:layout_height="18sp"
|
||||
android:contentDescription="@string/origin_format"
|
||||
android:src="@drawable/ic_web_outline"
|
||||
tools:ignore="RtlHardcoded,RtlSymmetry" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_origin"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="2dp"
|
||||
android:ellipsize="end"
|
||||
android:paddingRight="6dp"
|
||||
android:singleLine="true"
|
||||
android:textSize="14sp"
|
||||
tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
|
||||
tools:text="@string/origin_format" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_info"
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cd_info"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="2"
|
||||
android:background="@drawable/bg_gradient_mask_land"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:layout_marginVertical="16dp"
|
||||
android:layout_weight="5"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<HorizontalScrollView
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/ll_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:scrollbars="none">
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@drawable/bg_gradient_mask_land"
|
||||
android:padding="8dp">
|
||||
|
||||
<io.legato.kazusa.ui.widget.LabelsBar
|
||||
android:id="@+id/lb_kind"
|
||||
android:layout_width="wrap_content"
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/scroll_labels"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible" />
|
||||
android:scrollbars="none"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
</HorizontalScrollView>
|
||||
<io.legato.kazusa.ui.widget.LabelsBar
|
||||
android:id="@+id/lb_kind"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
</HorizontalScrollView>
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.button.MaterialButtonGroup
|
||||
android:id="@+id/cl_buttons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/scroll_labels">
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_shelf"
|
||||
style="@style/Widget.Material3Expressive.Button.TonalButton"
|
||||
android:backgroundTint="?attr/colorSurfaceContainer"
|
||||
style="@style/Widget.Material3Expressive.Button.TextButton"
|
||||
app:cornerRadius="16dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:padding="8dp"
|
||||
android:text="@string/remove_from_bookshelf"
|
||||
android:textSize="13sp"
|
||||
app:icon="@drawable/ic_clear_all"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="20dp" />
|
||||
app:iconSize="20dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- 查看目录按钮 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tv_toc_view"
|
||||
style="@style/Widget.Material3Expressive.Button.TonalButton"
|
||||
android:backgroundTint="?attr/colorSurfaceContainer"
|
||||
style="@style/Widget.Material3Expressive.Button.TextButton"
|
||||
app:cornerRadius="16dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:padding="8dp"
|
||||
android:text="@string/view_toc"
|
||||
android:textSize="13sp"
|
||||
app:icon="@drawable/ic_toc"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="20dp" />
|
||||
app:iconSize="20dp"
|
||||
app:layout_constraintStart_toEndOf="@id/btn_shelf"
|
||||
app:layout_constraintTop_toTopOf="@id/btn_shelf" />
|
||||
|
||||
<!-- 更换分组按钮 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tv_change_group"
|
||||
style="@style/Widget.Material3Expressive.Button.TonalButton"
|
||||
android:backgroundTint="?attr/colorSurfaceContainer"
|
||||
style="@style/Widget.Material3Expressive.Button.TextButton"
|
||||
app:cornerRadius="16dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:padding="8dp"
|
||||
android:text="@string/change_group"
|
||||
android:textSize="13sp"
|
||||
app:icon="@drawable/ic_groups"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="20dp" />
|
||||
app:iconSize="20dp"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_toc_view"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_toc_view" />
|
||||
|
||||
<!-- 更换来源按钮(保持原样) -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_change_source"
|
||||
style="@style/Widget.Material3Expressive.Button.TonalButton"
|
||||
android:backgroundTint="?attr/colorSurfaceContainer"
|
||||
style="@style/Widget.Material3Expressive.Button.TextButton"
|
||||
app:cornerRadius="16dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/book_source"
|
||||
android:padding="8dp"
|
||||
app:icon="@drawable/ic_change_source"
|
||||
android:text="@string/book_source"
|
||||
android:textSize="13sp"
|
||||
app:icon="@drawable/ic_change_source"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="20dp"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_change_group"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_change_group"
|
||||
tools:contentDescription="@string/change_origin" />
|
||||
</com.google.android.material.button.MaterialButtonGroup>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<TextView
|
||||
android:id="@+id/tv_toc"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="16dp"
|
||||
android:paddingHorizontal="2dp"
|
||||
android:orientation="vertical">
|
||||
android:layout_marginTop="8dp"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cl_buttons"
|
||||
tools:text="@string/toc_s" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_toc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="6dp"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="@string/toc_s" />
|
||||
<TextView
|
||||
android:id="@+id/tv_lasted"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:singleLine="true"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_toc"
|
||||
tools:text="@string/read_dur_progress" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_lasted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="6dp"
|
||||
android:singleLine="true"
|
||||
android:textSize="14sp"
|
||||
tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
|
||||
tools:text="@string/read_dur_progress" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/tv_chapter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_lasted"
|
||||
tools:text="@string/read_chapter_total" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_chapter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:singleLine="true"
|
||||
android:paddingEnd="8dp"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
|
||||
tools:text="@string/read_chapter_total" />
|
||||
<TextView
|
||||
android:id="@+id/div"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="|"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/tv_chapter"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_chapter" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:text="|"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
android:layout_marginEnd="8dp"/>
|
||||
<TextView
|
||||
android:id="@+id/tv_chapter_index"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/tv_chapter"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/div"
|
||||
tools:text="@string/read_chapter_index" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_chapter_index"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
android:singleLine="true"
|
||||
android:textSize="14sp"
|
||||
tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
|
||||
tools:text="@string/read_chapter_index"/>
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/tv_detail"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:minHeight="48dp"
|
||||
android:paddingBottom="80dp"
|
||||
android:text="@string/book_intro"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="14sp"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_chapter"
|
||||
tools:text="@string/book_intro" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="48dp"
|
||||
android:paddingBottom="80dp"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:text="@string/book_intro"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="14sp"
|
||||
android:visibility="visible"
|
||||
tools:ignore="RtlHardcoded,RtlSymmetry" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
@@ -363,15 +370,13 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginEnd="40dp"
|
||||
android:minWidth="0dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_margin="48dp"
|
||||
android:contentDescription="@string/reading"
|
||||
android:minWidth="56dp"
|
||||
android:text="@string/reading"
|
||||
android:textColor="?attr/colorOnPrimary"
|
||||
android:contentDescription="@string/reading"
|
||||
app:iconGravity="textStart"
|
||||
app:backgroundTint="?attr/colorTertiary"
|
||||
app:icon="@drawable/ic_bottom_books"
|
||||
app:backgroundTint="?attr/colorSecondary"
|
||||
app:iconTint="?attr/colorOnPrimary" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -14,16 +14,11 @@
|
||||
android:scaleType="centerCrop"
|
||||
tools:ignore="ContentDescription,ImageContrastCheck" />
|
||||
|
||||
<View
|
||||
android:id="@+id/vw_bg"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/vw_bg2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0.80"
|
||||
android:alpha="0.70"
|
||||
android:background="?attr/colorSurfaceContainer" />
|
||||
|
||||
<io.legato.kazusa.ui.widget.TitleBar
|
||||
@@ -194,8 +189,6 @@
|
||||
android:paddingHorizontal="6dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/ll_sub_menu">
|
||||
|
||||
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
|
||||
@@ -10,9 +10,16 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/bg_image"
|
||||
android:foreground="@drawable/bg_book_mask"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bg_book_mask"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0.6"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:contentDescription="@string/bg_image" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
@@ -21,6 +28,7 @@
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/coll_top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
|
||||
style="?attr/collapsingToolbarLayoutMediumStyle"
|
||||
@@ -56,32 +64,52 @@
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/ll_cover"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_gradient_mask"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="8dp">
|
||||
android:paddingVertical="8dp"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cd_cov"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="5:7"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.32"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<io.legato.kazusa.ui.widget.image.CoverImageView
|
||||
android:id="@+id/iv_cover"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="140dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/img_cover"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/image_cover_default" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
android:padding="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/cd_cov"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/cd_cov"
|
||||
app:layout_constraintTop_toTopOf="@id/cd_cov">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
@@ -102,6 +130,7 @@
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_name"
|
||||
android:layout_width="18sp"
|
||||
android:layout_height="18sp"
|
||||
android:contentDescription="@string/author"
|
||||
@@ -152,189 +181,195 @@
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/ll_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ll_cover">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:scrollbars="none">
|
||||
|
||||
<io.legato.kazusa.ui.widget.LabelsBar
|
||||
android:id="@+id/lb_kind"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<LinearLayout
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/scroll_labels"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="8dp">
|
||||
android:paddingStart="16dp"
|
||||
android:scrollbars="none"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<io.legato.kazusa.ui.widget.LabelsBar
|
||||
android:id="@+id/lb_kind"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:weightSum="4"
|
||||
android:gravity="center_vertical">
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_shelf"
|
||||
style="@style/Widget.Material3Expressive.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:text="@string/remove_from_bookshelf"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/ic_clear_all"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="20dp" />
|
||||
</HorizontalScrollView>
|
||||
|
||||
<!-- 查看目录按钮 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tv_toc_view"
|
||||
style="@style/Widget.Material3Expressive.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:padding="8dp"
|
||||
android:text="@string/view_toc"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/ic_toc"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="20dp" />
|
||||
<LinearLayout
|
||||
android:id="@+id/cl_buttons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="4"
|
||||
app:layout_constraintTop_toBottomOf="@id/scroll_labels">
|
||||
|
||||
<!-- 更换分组按钮 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tv_change_group"
|
||||
style="@style/Widget.Material3Expressive.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:text="@string/change_group"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/ic_groups"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="20dp" />
|
||||
|
||||
<!-- 更换来源按钮(保持原样) -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_change_source"
|
||||
style="@style/Widget.Material3Expressive.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/book_source"
|
||||
android:padding="8dp"
|
||||
app:icon="@drawable/ic_change_source"
|
||||
android:textSize="12sp"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="20dp"
|
||||
tools:contentDescription="@string/change_origin" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_shelf"
|
||||
style="@style/Widget.Material3Expressive.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:orientation="vertical">
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:text="@string/remove_from_bookshelf"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/ic_clear_all"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="20dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_toc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="6dp"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="@string/toc_s" />
|
||||
<!-- 查看目录按钮 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tv_toc_view"
|
||||
style="@style/Widget.Material3Expressive.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:text="@string/view_toc"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/ic_toc"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="20dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_lasted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="6dp"
|
||||
android:textSize="14sp"
|
||||
tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
|
||||
tools:text="@string/read_dur_progress" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_chapter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/colorPrimaryVariant"
|
||||
android:singleLine="true"
|
||||
android:paddingEnd="8dp"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
|
||||
tools:text="@string/read_chapter_total" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:text="|"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
android:layout_marginEnd="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_chapter_index"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
android:singleLine="true"
|
||||
android:textSize="14sp"
|
||||
tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry"
|
||||
tools:text="@string/read_chapter_index"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
<!-- 更换分组按钮 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tv_change_group"
|
||||
style="@style/Widget.Material3Expressive.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:text="@string/change_group"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/ic_groups"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="20dp" />
|
||||
|
||||
<!-- 更换来源按钮(保持原样) -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_change_source"
|
||||
style="@style/Widget.Material3Expressive.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:text="@string/book_source"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/ic_change_source"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="20dp"
|
||||
tools:contentDescription="@string/change_origin" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail"
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/tv_toc"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cl_buttons"
|
||||
tools:text="@string/toc_s" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_lasted"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:singleLine="true"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_toc"
|
||||
tools:text="@string/read_dur_progress" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_chapter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_lasted"
|
||||
tools:text="@string/read_chapter_total" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/div"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="|"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/tv_chapter"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_chapter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_chapter_index"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/tv_chapter"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/div"
|
||||
tools:text="@string/read_chapter_index" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="48dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingBottom="80dp"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:minHeight="48dp"
|
||||
android:paddingBottom="120dp"
|
||||
android:text="@string/book_intro"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="14sp"
|
||||
android:visibility="visible"
|
||||
tools:ignore="RtlHardcoded,RtlSymmetry" />
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_chapter"
|
||||
tools:text="@string/book_intro" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user