Просмотр исходного кода

增加[紧凑网格]、[封面网格]书架布局。

HapeLee 1 год назад
Родитель
Сommit
02bc1845a0

+ 8 - 2
app/src/main/java/io/legato/kazusa/constant/PreferKey.kt

@@ -37,8 +37,8 @@ object PreferKey {
     const val enableReview = "enableReview"
     const val showRss = "showRss"
     const val bookshelfLayout = "bookshelfLayout"
-    const val bookshelfLayoutPortrait = "bookshelfLayoutPortrait"
-    const val bookshelfLayoutLandscape = "bookshelf_layout_landscape"
+    const val bookshelfLayoutModePortrait = "bookshelfLayoutPortrait"
+    const val bookshelfLayoutModeLandscape = "bookshelf_layout_landscape"
     const val bookshelfSort = "bookshelfSort"
     const val bookExportFileName = "bookExportFileName"
     const val bookImportFileName = "bookImportFileName"
@@ -206,4 +206,10 @@ object PreferKey {
     const val notificationsPost = "notificationsPost"
     const val ignoreBatteryPermission = "ignoreBatteryPermission"
     const val paddingDisplayCutouts = "paddingDisplayCutouts"
+
+    const val delayBookLoadEnable = "delayBookLoadEnable"
+
+    const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape"
+
+    const val bookshelfLayoutGridPortrait = "bookshelfLayoutGridPortrait"
 }

+ 43 - 28
app/src/main/java/io/legato/kazusa/ui/main/bookshelf/BookshelfConfigBottomSheet.kt

@@ -13,6 +13,7 @@ import io.legato.kazusa.constant.EventBus
 import io.legato.kazusa.databinding.DialogBookshelfConfigBinding
 import io.legato.kazusa.help.config.AppConfig
 import io.legato.kazusa.ui.main.MainViewModel
+import io.legato.kazusa.utils.bookshelfLayoutGrid
 import io.legato.kazusa.utils.postEvent
 import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
 
@@ -26,13 +27,18 @@ class BookshelfConfigBottomSheet : BaseBottomSheetDialogFragment(R.layout.dialog
         val orientation = requireContext().resources.configuration.orientation
 
         val bookshelfLayout = if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
-            AppConfig.bookshelfLayoutLandscape
+            AppConfig.bookshelfLayoutModeLandscape
         } else {
-            AppConfig.bookshelfLayoutPortrait
+            AppConfig.bookshelfLayoutModePortrait
         }
 
-        val isGrid = bookshelfLayout > 0
-        val columnCount = bookshelfLayout.takeIf { it > 0 } ?: 1
+        val isList = bookshelfLayout == 0
+        val isGrid = bookshelfLayout == 1
+        val isGridCompact = bookshelfLayout == 2
+        val isGridCover = bookshelfLayout == 3
+
+        val columnCount = requireContext().bookshelfLayoutGrid.takeIf { it > 0 } ?: 1
+
 
         val bookshelfSort = AppConfig.bookshelfSort
 
@@ -69,10 +75,12 @@ class BookshelfConfigBottomSheet : BaseBottomSheetDialogFragment(R.layout.dialog
             swShowWaitUpBooks.isChecked = AppConfig.showWaitUpCount
             swShowBookshelfFastScroller.isChecked = AppConfig.showBookshelfFastScroller
 
-            chipList.isChecked = !isGrid
+            chipList.isChecked = isList
             chipGrid.isChecked = isGrid
-            sliderText.isVisible = isGrid
-            sliderGridCount.isVisible = isGrid
+            chipGridCompact.isChecked = isGridCompact
+            chipGridCover.isChecked = isGridCover
+            sliderText.isVisible = isGrid || isGridCompact || isGridCover
+            sliderGridCount.isVisible = isGrid || isGridCompact || isGridCover
             sliderGridCount.value = columnCount.toFloat()
 
             when (AppConfig.bookshelfSortOrder) {
@@ -80,10 +88,13 @@ class BookshelfConfigBottomSheet : BaseBottomSheetDialogFragment(R.layout.dialog
                 1 -> binding.chipGroupOrder.check(binding.chipDesc.id)
             }
 
-            chipGroupLayout.setOnCheckedStateChangeListener { group, checkedIds ->
-                sliderText.isVisible = checkedIds.firstOrNull() == R.id.chip_grid
+            chipGroupLayout.setOnCheckedStateChangeListener { _, checkedIds ->
+                val checkedId = checkedIds.firstOrNull()
+                val showSlider =
+                    checkedId == R.id.chip_grid || checkedId == R.id.chip_grid_compact || checkedId == R.id.chip_grid_cover
+                sliderText.isVisible = showSlider
                 TransitionManager.beginDelayedTransition(root)
-                sliderGridCount.isVisible = checkedIds.firstOrNull() == R.id.chip_grid
+                sliderGridCount.isVisible = showSlider
             }
 
             btnOk.setOnClickListener {
@@ -91,7 +102,9 @@ class BookshelfConfigBottomSheet : BaseBottomSheetDialogFragment(R.layout.dialog
                 var recreate = false
 
                 if (AppConfig.bookGroupStyle != chipGroupStyle.checkedChipId) {
-                    AppConfig.bookGroupStyle = chipGroupStyle.indexOfChild(chipGroupStyle.findViewById(chipGroupStyle.checkedChipId))
+                    AppConfig.bookGroupStyle = chipGroupStyle.indexOfChild(
+                        chipGroupStyle.findViewById(chipGroupStyle.checkedChipId)
+                    )
                     notifyMain = true
                 }
 
@@ -107,7 +120,6 @@ class BookshelfConfigBottomSheet : BaseBottomSheetDialogFragment(R.layout.dialog
                     AppConfig.showWaitUpCount = swShowWaitUpBooks.isChecked
                     activityViewModel.postUpBooksLiveData(true)
                 }
-
                 if (AppConfig.showBookshelfFastScroller != swShowBookshelfFastScroller.isChecked) {
                     AppConfig.showBookshelfFastScroller = swShowBookshelfFastScroller.isChecked
                     postEvent(EventBus.BOOKSHELF_REFRESH, "")
@@ -118,25 +130,29 @@ class BookshelfConfigBottomSheet : BaseBottomSheetDialogFragment(R.layout.dialog
                     (requireParentFragment() as? BaseBookshelfFragment)?.upSort()
                 }
 
-                val isNowGrid = chipGrid.isChecked
                 val selectedColumn = sliderGridCount.value.toInt()
-                val newLayout = if (isNowGrid) selectedColumn else 0
+                val newLayout = when {
+                    chipList.isChecked -> 0
+                    chipGrid.isChecked -> 1
+                    chipGridCompact.isChecked -> 2
+                    chipGridCover.isChecked -> 3
+                    else -> 0
+                }
 
-                if (bookshelfLayout != newLayout) {
+                val oldColumn = requireContext().bookshelfLayoutGrid
+                if (bookshelfLayout != newLayout || oldColumn != selectedColumn) {
                     if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
-                        AppConfig.bookshelfLayoutLandscape = newLayout
-                        if (newLayout == 0) {
-                            activityViewModel.booksGridRecycledViewPool.clear()
-                        } else {
-                            activityViewModel.booksListRecycledViewPool.clear()
-                        }
+                        AppConfig.bookshelfLayoutModeLandscape = newLayout
+                        AppConfig.bookshelfLayoutGridLandscape = selectedColumn
                     } else {
-                        AppConfig.bookshelfLayoutPortrait = newLayout
-                        if (newLayout == 0) {
-                            activityViewModel.booksGridRecycledViewPool.clear()
-                        } else {
-                            activityViewModel.booksListRecycledViewPool.clear()
-                        }
+                        AppConfig.bookshelfLayoutModePortrait = newLayout
+                        AppConfig.bookshelfLayoutGridPortrait = selectedColumn
+                    }
+
+                    if (newLayout == 0) {
+                        activityViewModel.booksGridRecycledViewPool.clear()
+                    } else {
+                        activityViewModel.booksListRecycledViewPool.clear()
                     }
                     recreate = true
                 }
@@ -146,7 +162,6 @@ class BookshelfConfigBottomSheet : BaseBottomSheetDialogFragment(R.layout.dialog
                     binding.chipDesc.id -> 1
                     else -> 1
                 }
-
                 if (AppConfig.bookshelfSortOrder != newOrder) {
                     AppConfig.bookshelfSortOrder = newOrder
                     (requireParentFragment() as? BaseBookshelfFragment)?.upSort()

+ 25 - 12
app/src/main/java/io/legato/kazusa/ui/main/bookshelf/books/BooksFragment.kt

@@ -36,8 +36,11 @@ import io.legato.kazusa.ui.book.read.ReadBookActivity
 import io.legato.kazusa.ui.main.MainViewModel
 import io.legato.kazusa.ui.main.bookshelf.books.styleDefalut.BaseBooksAdapter
 import io.legato.kazusa.ui.main.bookshelf.books.styleDefalut.BooksAdapterGrid
+import io.legato.kazusa.ui.main.bookshelf.books.styleDefalut.BooksAdapterGridCompact
+import io.legato.kazusa.ui.main.bookshelf.books.styleDefalut.BooksAdapterGridCover
 import io.legato.kazusa.ui.main.bookshelf.books.styleDefalut.BooksAdapterList
-import io.legato.kazusa.utils.bookshelfLayout
+import io.legato.kazusa.utils.bookshelfLayoutGrid
+import io.legato.kazusa.utils.bookshelfLayoutMode
 import io.legato.kazusa.utils.cnCompare
 import io.legato.kazusa.utils.flowWithLifecycleAndDatabaseChangeFirst
 import io.legato.kazusa.utils.observeEvent
@@ -71,14 +74,27 @@ class BooksFragment() : BaseFragment(R.layout.fragment_books),
     private val binding by viewBinding(FragmentBooksBinding::bind)
     private val activityViewModel by activityViewModels<MainViewModel>()
 
-    private val bookshelfLayout by lazy { requireContext().bookshelfLayout }
+    private val bookshelfLayoutMode by lazy { requireContext().bookshelfLayoutMode }
 
+    private val bookshelfLayoutGrid by lazy { requireContext().bookshelfLayoutGrid }
 
     private val booksAdapter: BaseBooksAdapter<*> by lazy {
-        if (bookshelfLayout == 0) {
-            BooksAdapterList(requireContext(), this, this, viewLifecycleOwner.lifecycle)
-        } else {
-            BooksAdapterGrid(requireContext(), this)
+        when (bookshelfLayoutMode) {
+            0 -> {
+                BooksAdapterList(requireContext(), this, this, viewLifecycleOwner.lifecycle)
+            }
+
+            1 -> {
+                BooksAdapterGrid(requireContext(), this)
+            }
+
+            2 -> {
+                BooksAdapterGridCompact(requireContext(), this)
+            }
+
+            else -> {
+                BooksAdapterGridCover(requireContext(), this)
+            }
         }
     }
     private var booksFlowJob: Job? = null
@@ -111,14 +127,11 @@ class BooksFragment() : BaseFragment(R.layout.fragment_books),
             binding.refreshLayout.isRefreshing = false
             activityViewModel.upToc(booksAdapter.getItems())
         }
-        if (bookshelfLayout == 0) {
+        if (bookshelfLayoutMode == 0) {
             binding.rvBookshelf.layoutManager = LinearLayoutManager(context)
-        } else {
-            binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayout)
-        }
-        if (bookshelfLayout == 0) {
             binding.rvBookshelf.setRecycledViewPool(activityViewModel.booksListRecycledViewPool)
         } else {
+            binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayoutGrid)
             binding.rvBookshelf.setRecycledViewPool(activityViewModel.booksGridRecycledViewPool)
         }
         booksAdapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
@@ -219,7 +232,7 @@ class BooksFragment() : BaseFragment(R.layout.fragment_books),
 
     private fun startLastUpdateTimeJob() {
         upLastUpdateTimeJob?.cancel()
-        if (!AppConfig.showLastUpdateTime || bookshelfLayout != 0) {
+        if (!AppConfig.showLastUpdateTime || bookshelfLayoutMode != 0) {
             return
         }
         upLastUpdateTimeJob = lifecycleScope.launch {

+ 25 - 8
app/src/main/java/io/legato/kazusa/ui/main/bookshelf/books/BookshelfFragment2.kt

@@ -33,8 +33,11 @@ import io.legato.kazusa.ui.book.search.SearchActivity
 import io.legato.kazusa.ui.main.bookshelf.BaseBookshelfFragment
 import io.legato.kazusa.ui.main.bookshelf.books.styleFold.BaseBooksAdapter
 import io.legato.kazusa.ui.main.bookshelf.books.styleFold.BooksAdapterGrid
+import io.legato.kazusa.ui.main.bookshelf.books.styleFold.BooksAdapterGridCompact
+import io.legato.kazusa.ui.main.bookshelf.books.styleFold.BooksAdapterGridCover
 import io.legato.kazusa.ui.main.bookshelf.books.styleFold.BooksAdapterList
-import io.legato.kazusa.utils.bookshelfLayout
+import io.legato.kazusa.utils.bookshelfLayoutGrid
+import io.legato.kazusa.utils.bookshelfLayoutMode
 import io.legato.kazusa.utils.cnCompare
 import io.legato.kazusa.utils.flowWithLifecycleAndDatabaseChangeFirst
 import io.legato.kazusa.utils.observeEvent
@@ -64,13 +67,27 @@ class BookshelfFragment2() : BaseBookshelfFragment(R.layout.fragment_bookshelf2)
 
     private val binding by viewBinding(FragmentBookshelf2Binding::bind)
 
-    private val bookshelfLayout by lazy { requireContext().bookshelfLayout }
+    private val bookshelfLayoutMode by lazy { requireContext().bookshelfLayoutMode }
+
+    private val bookshelfLayoutGrid by lazy { requireContext().bookshelfLayoutGrid }
 
     private val booksAdapter: BaseBooksAdapter<*> by lazy {
-        if (bookshelfLayout == 0) {
-            BooksAdapterList(requireContext(), this)
-        } else {
-            BooksAdapterGrid(requireContext(), this)
+        when (bookshelfLayoutMode) {
+            0 -> {
+                BooksAdapterList(requireContext(), this)
+            }
+
+            1 -> {
+                BooksAdapterGrid(requireContext(), this)
+            }
+
+            2 -> {
+                BooksAdapterGridCompact(requireContext(), this)
+            }
+
+            else -> {
+                BooksAdapterGridCover(requireContext(), this)
+            }
         }
     }
 
@@ -94,10 +111,10 @@ class BookshelfFragment2() : BaseBookshelfFragment(R.layout.fragment_bookshelf2)
             binding.refreshLayout.isRefreshing = false
             activityViewModel.upToc(books)
         }
-        if (bookshelfLayout == 0) {
+        if (bookshelfLayoutMode == 0) {
             binding.rvBookshelf.layoutManager = LinearLayoutManager(context)
         } else {
-            binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayout)
+            binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayoutGrid)
         }
         binding.rvBookshelf.itemAnimator = null
         binding.rvBookshelf.adapter = booksAdapter

+ 90 - 0
app/src/main/java/io/legato/kazusa/ui/main/bookshelf/books/styleDefalut/BooksAdapterGridCompact.kt

@@ -0,0 +1,90 @@
+package io.legato.kazusa.ui.main.bookshelf.books.styleDefalut
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import io.legato.kazusa.base.adapter.ItemViewHolder
+import io.legato.kazusa.data.entities.Book
+import io.legato.kazusa.databinding.ItemBookshelfGridCompactBinding
+import io.legato.kazusa.help.book.isLocal
+import io.legato.kazusa.help.config.AppConfig
+
+class BooksAdapterGridCompact(context: Context, private val callBack: CallBack) :
+    BaseBooksAdapter<ItemBookshelfGridCompactBinding>(context) {
+
+    override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridCompactBinding {
+        return ItemBookshelfGridCompactBinding.inflate(inflater, parent, false)
+    }
+
+    override fun convert(
+        holder: ItemViewHolder,
+        binding: ItemBookshelfGridCompactBinding,
+        item: Book,
+        payloads: MutableList<Any>
+    ) = binding.run {
+        binding.ivCover.transitionName = "book_${item.bookUrl}"
+        if (payloads.isEmpty()) {
+            tvName.text = item.name
+            ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
+            upRefresh(binding, item)
+        } else {
+            for (i in payloads.indices) {
+                val bundle = payloads[i] as Bundle
+                bundle.keySet().forEach {
+                    when (it) {
+                        "name" -> tvName.text = item.name
+                        "cover" -> ivCover.load(
+                            item.getDisplayCover(),
+                            item.name,
+                            item.author,
+                            false,
+                            item.origin
+                        )
+
+                        "refresh" -> upRefresh(binding, item)
+                    }
+                }
+            }
+        }
+    }
+
+    private fun upRefresh(binding: ItemBookshelfGridCompactBinding, item: Book) {
+        if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
+            binding.cdUnread.visibility = View.GONE
+            binding.rlLoading.visibility = View.VISIBLE
+        } else {
+            binding.rlLoading.visibility = View.GONE
+            if (AppConfig.showUnread) {
+                val unreadCount = item.getUnreadChapterNum()
+                if (unreadCount > 0) {
+                    binding.cdUnread.visibility = View.VISIBLE
+                    binding.tvUnread.text = unreadCount.toString()
+                } else {
+                    binding.cdUnread.visibility = View.GONE
+                }
+            } else {
+                binding.cdUnread.visibility = View.GONE
+            }
+        }
+    }
+
+    override fun registerListener(
+        holder: ItemViewHolder,
+        binding: ItemBookshelfGridCompactBinding
+    ) {
+
+        binding.cvContent.setOnClickListener {
+            getItem(holder.layoutPosition)?.let {
+                callBack.open(it, binding.ivCover)
+            }
+        }
+
+        binding.cvContent.setOnLongClickListener {
+            getItem(holder.layoutPosition)?.let {
+                callBack.openBookInfo(it, binding.ivCover)
+            }
+            true
+        }
+    }
+}

+ 91 - 0
app/src/main/java/io/legato/kazusa/ui/main/bookshelf/books/styleDefalut/BooksAdapterGridCover.kt

@@ -0,0 +1,91 @@
+package io.legato.kazusa.ui.main.bookshelf.books.styleDefalut
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import io.legato.kazusa.base.adapter.ItemViewHolder
+import io.legato.kazusa.data.entities.Book
+import io.legato.kazusa.databinding.ItemBookshelfGridCompactBinding
+import io.legato.kazusa.help.book.isLocal
+import io.legato.kazusa.help.config.AppConfig
+import io.legato.kazusa.utils.gone
+
+class BooksAdapterGridCover(context: Context, private val callBack: CallBack) :
+    BaseBooksAdapter<ItemBookshelfGridCompactBinding>(context) {
+
+    override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridCompactBinding {
+        return ItemBookshelfGridCompactBinding.inflate(inflater, parent, false)
+    }
+
+    override fun convert(
+        holder: ItemViewHolder,
+        binding: ItemBookshelfGridCompactBinding,
+        item: Book,
+        payloads: MutableList<Any>
+    ) = binding.run {
+        binding.ivCover.transitionName = "book_${item.bookUrl}"
+        if (payloads.isEmpty()) {
+            tvName.gone()
+            ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
+            upRefresh(binding, item)
+        } else {
+            for (i in payloads.indices) {
+                val bundle = payloads[i] as Bundle
+                bundle.keySet().forEach {
+                    when (it) {
+                        "name" -> tvName.gone()
+                        "cover" -> ivCover.load(
+                            item.getDisplayCover(),
+                            item.name,
+                            item.author,
+                            false,
+                            item.origin
+                        )
+
+                        "refresh" -> upRefresh(binding, item)
+                    }
+                }
+            }
+        }
+    }
+
+    private fun upRefresh(binding: ItemBookshelfGridCompactBinding, item: Book) {
+        if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
+            binding.cdUnread.visibility = View.GONE
+            binding.rlLoading.visibility = View.VISIBLE
+        } else {
+            binding.rlLoading.visibility = View.GONE
+            if (AppConfig.showUnread) {
+                val unreadCount = item.getUnreadChapterNum()
+                if (unreadCount > 0) {
+                    binding.cdUnread.visibility = View.VISIBLE
+                    binding.tvUnread.text = unreadCount.toString()
+                } else {
+                    binding.cdUnread.visibility = View.GONE
+                }
+            } else {
+                binding.cdUnread.visibility = View.GONE
+            }
+        }
+    }
+
+    override fun registerListener(
+        holder: ItemViewHolder,
+        binding: ItemBookshelfGridCompactBinding
+    ) {
+
+        binding.cvContent.setOnClickListener {
+            getItem(holder.layoutPosition)?.let {
+                callBack.open(it, binding.ivCover)
+            }
+        }
+
+        binding.cvContent.setOnLongClickListener {
+            getItem(holder.layoutPosition)?.let {
+                callBack.openBookInfo(it, binding.ivCover)
+            }
+            true
+        }
+    }
+}

+ 190 - 0
app/src/main/java/io/legato/kazusa/ui/main/bookshelf/books/styleFold/BooksAdapterGridCompact.kt

@@ -0,0 +1,190 @@
+package io.legato.kazusa.ui.main.bookshelf.books.styleFold
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import io.legato.kazusa.R
+import io.legato.kazusa.data.entities.Book
+import io.legato.kazusa.data.entities.BookGroup
+import io.legato.kazusa.databinding.ItemBookshelfGridCompactBinding
+import io.legato.kazusa.databinding.ItemBookshelfGridCompactGroupBinding
+import io.legato.kazusa.help.book.isLocal
+import io.legato.kazusa.help.config.AppConfig
+import io.legato.kazusa.utils.gone
+import splitties.views.onLongClick
+
+@Suppress("UNUSED_PARAMETER")
+class BooksAdapterGridCompact(context: Context, callBack: CallBack) :
+    BaseBooksAdapter<RecyclerView.ViewHolder>(context, callBack) {
+
+    override fun onCreateViewHolder(
+        parent: ViewGroup,
+        viewType: Int
+    ): RecyclerView.ViewHolder {
+        return when (viewType) {
+            1 -> GroupViewHolder(
+                ItemBookshelfGridCompactGroupBinding.inflate(
+                    inflater,
+                    parent,
+                    false
+                )
+            )
+
+            else -> BookViewHolder(ItemBookshelfGridCompactBinding.inflate(inflater, parent, false))
+        }
+    }
+
+    override fun onBindViewHolder(
+        holder: RecyclerView.ViewHolder,
+        position: Int,
+        payloads: MutableList<Any>
+    ) {
+
+        when (holder) {
+            is BookViewHolder -> (getItem(position) as? Book)?.let {
+                holder.registerListener(it)
+                holder.onBind(it, payloads)
+            }
+
+            is GroupViewHolder -> (getItem(position) as? BookGroup)?.let {
+                holder.registerListener(it)
+                holder.onBind(it, payloads)
+            }
+        }
+    }
+
+    private fun getBooksForGroup(group: BookGroup): List<Book> {
+        return if (group.groupId == -1L) {
+            allBooks.take(4)
+        } else {
+            allBooks.filter { (it.group and group.groupId) != 0L }.take(4)
+        }
+    }
+
+    inner class BookViewHolder(val binding: ItemBookshelfGridCompactBinding) :
+        RecyclerView.ViewHolder(binding.root) {
+
+        fun onBind(item: Book) = binding.run {
+            tvName.text = item.name
+            ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
+            upRefresh(this, item)
+        }
+
+        fun onBind(item: Book, payloads: MutableList<Any>) = binding.run {
+            if (payloads.isEmpty()) {
+                onBind(item)
+            } else {
+                for (i in payloads.indices) {
+                    val bundle = payloads[i] as Bundle
+                    bundle.keySet().forEach {
+                        when (it) {
+                            "name" -> tvName.text = item.name
+                            "cover" -> ivCover.load(
+                                item.getDisplayCover(),
+                                item.name,
+                                item.author,
+                                false,
+                                item.origin
+                            )
+
+                            "refresh" -> upRefresh(this, item)
+                        }
+                    }
+                }
+            }
+        }
+
+        fun registerListener(item: Any) {
+            binding.cvContent.setOnClickListener {
+                callBack.onItemClick(item, binding.ivCover)
+            }
+            binding.cvContent.onLongClick {
+                callBack.onItemLongClick(item, binding.ivCover)
+            }
+        }
+
+        private fun upRefresh(binding: ItemBookshelfGridCompactBinding, item: Book) {
+            if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
+                binding.cdUnread.visibility = View.GONE
+                binding.rlLoading.visibility = View.VISIBLE
+            } else {
+                binding.rlLoading.visibility = View.GONE
+                if (AppConfig.showUnread) {
+                    val unreadCount = item.getUnreadChapterNum()
+                    if (unreadCount > 0) {
+                        binding.cdUnread.visibility = View.VISIBLE
+                        binding.tvUnread.text = unreadCount.toString()
+                    } else {
+                        binding.cdUnread.visibility = View.GONE
+                    }
+                } else {
+                    binding.cdUnread.visibility = View.GONE
+                }
+            }
+        }
+
+
+    }
+
+    inner class GroupViewHolder(val binding: ItemBookshelfGridCompactGroupBinding) :
+        RecyclerView.ViewHolder(binding.root) {
+
+        fun onBind(item: BookGroup) = binding.run {
+            tvName.text = item.groupName
+            loadGroupCover(item)
+            cdUnread.gone()
+        }
+
+        fun onBind(item: BookGroup, payloads: MutableList<Any>) = binding.run {
+            if (payloads.isEmpty()) {
+                onBind(item)
+            } else {
+                for (i in payloads.indices) {
+                    val bundle = payloads[i] as Bundle
+                    bundle.keySet().forEach {
+                        when (it) {
+                            "groupName" -> tvName.text = item.groupName
+                            "cover" -> loadGroupCover(item)
+                        }
+                    }
+                }
+            }
+        }
+
+        fun registerListener(item: Any) {
+            binding.cvContent.setOnClickListener {
+                callBack.onItemClick(item, binding.ivCover)
+            }
+            binding.cvContent.onLongClick {
+                callBack.onItemLongClick(item, binding.ivCover)
+            }
+        }
+
+        private fun loadGroupCover(item: BookGroup) = binding.run {
+
+            binding.ivCover.setImageDrawable(
+                ContextCompat.getDrawable(binding.root.context, R.drawable.bg_surface_variant)
+            )
+            binding.ivCover1.setImageDrawable(null)
+            binding.ivCover2.setImageDrawable(null)
+            binding.ivCover3.setImageDrawable(null)
+            binding.ivCover4.setImageDrawable(null)
+            if (!item.cover.isNullOrEmpty()) {
+                ivCover.load(item.cover)
+            } else {
+
+                val books = getBooksForGroup(item)
+
+                if (books.size > 0) binding.ivCover1.load(books[0].getDisplayCover())
+                if (books.size > 1) binding.ivCover2.load(books[1].getDisplayCover())
+                if (books.size > 2) binding.ivCover3.load(books[2].getDisplayCover())
+                if (books.size > 3) binding.ivCover4.load(books[3].getDisplayCover())
+            }
+        }
+
+    }
+
+}

+ 190 - 0
app/src/main/java/io/legato/kazusa/ui/main/bookshelf/books/styleFold/BooksAdapterGridCover.kt

@@ -0,0 +1,190 @@
+package io.legato.kazusa.ui.main.bookshelf.books.styleFold
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import io.legato.kazusa.R
+import io.legato.kazusa.data.entities.Book
+import io.legato.kazusa.data.entities.BookGroup
+import io.legato.kazusa.databinding.ItemBookshelfGridCompactBinding
+import io.legato.kazusa.databinding.ItemBookshelfGridCompactGroupBinding
+import io.legato.kazusa.help.book.isLocal
+import io.legato.kazusa.help.config.AppConfig
+import io.legato.kazusa.utils.gone
+import splitties.views.onLongClick
+
+@Suppress("UNUSED_PARAMETER")
+class BooksAdapterGridCover(context: Context, callBack: CallBack) :
+    BaseBooksAdapter<RecyclerView.ViewHolder>(context, callBack) {
+
+    override fun onCreateViewHolder(
+        parent: ViewGroup,
+        viewType: Int
+    ): RecyclerView.ViewHolder {
+        return when (viewType) {
+            1 -> GroupViewHolder(
+                ItemBookshelfGridCompactGroupBinding.inflate(
+                    inflater,
+                    parent,
+                    false
+                )
+            )
+
+            else -> BookViewHolder(ItemBookshelfGridCompactBinding.inflate(inflater, parent, false))
+        }
+    }
+
+    override fun onBindViewHolder(
+        holder: RecyclerView.ViewHolder,
+        position: Int,
+        payloads: MutableList<Any>
+    ) {
+
+        when (holder) {
+            is BookViewHolder -> (getItem(position) as? Book)?.let {
+                holder.registerListener(it)
+                holder.onBind(it, payloads)
+            }
+
+            is GroupViewHolder -> (getItem(position) as? BookGroup)?.let {
+                holder.registerListener(it)
+                holder.onBind(it, payloads)
+            }
+        }
+    }
+
+    private fun getBooksForGroup(group: BookGroup): List<Book> {
+        return if (group.groupId == -1L) {
+            allBooks.take(4)
+        } else {
+            allBooks.filter { (it.group and group.groupId) != 0L }.take(4)
+        }
+    }
+
+    inner class BookViewHolder(val binding: ItemBookshelfGridCompactBinding) :
+        RecyclerView.ViewHolder(binding.root) {
+
+        fun onBind(item: Book) = binding.run {
+            tvName.gone()
+            ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
+            upRefresh(this, item)
+        }
+
+        fun onBind(item: Book, payloads: MutableList<Any>) = binding.run {
+            if (payloads.isEmpty()) {
+                onBind(item)
+            } else {
+                for (i in payloads.indices) {
+                    val bundle = payloads[i] as Bundle
+                    bundle.keySet().forEach {
+                        when (it) {
+                            "name" -> tvName.gone()
+                            "cover" -> ivCover.load(
+                                item.getDisplayCover(),
+                                item.name,
+                                item.author,
+                                false,
+                                item.origin
+                            )
+
+                            "refresh" -> upRefresh(this, item)
+                        }
+                    }
+                }
+            }
+        }
+
+        fun registerListener(item: Any) {
+            binding.cvContent.setOnClickListener {
+                callBack.onItemClick(item, binding.ivCover)
+            }
+            binding.cvContent.onLongClick {
+                callBack.onItemLongClick(item, binding.ivCover)
+            }
+        }
+
+        private fun upRefresh(binding: ItemBookshelfGridCompactBinding, item: Book) {
+            if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
+                binding.cdUnread.visibility = View.GONE
+                binding.rlLoading.visibility = View.VISIBLE
+            } else {
+                binding.rlLoading.visibility = View.GONE
+                if (AppConfig.showUnread) {
+                    val unreadCount = item.getUnreadChapterNum()
+                    if (unreadCount > 0) {
+                        binding.cdUnread.visibility = View.VISIBLE
+                        binding.tvUnread.text = unreadCount.toString()
+                    } else {
+                        binding.cdUnread.visibility = View.GONE
+                    }
+                } else {
+                    binding.cdUnread.visibility = View.GONE
+                }
+            }
+        }
+
+
+    }
+
+    inner class GroupViewHolder(val binding: ItemBookshelfGridCompactGroupBinding) :
+        RecyclerView.ViewHolder(binding.root) {
+
+        fun onBind(item: BookGroup) = binding.run {
+            tvName.gone()
+            loadGroupCover(item)
+            cdUnread.gone()
+        }
+
+        fun onBind(item: BookGroup, payloads: MutableList<Any>) = binding.run {
+            if (payloads.isEmpty()) {
+                onBind(item)
+            } else {
+                for (i in payloads.indices) {
+                    val bundle = payloads[i] as Bundle
+                    bundle.keySet().forEach {
+                        when (it) {
+                            "groupName" -> tvName.gone()
+                            "cover" -> loadGroupCover(item)
+                        }
+                    }
+                }
+            }
+        }
+
+        fun registerListener(item: Any) {
+            binding.cvContent.setOnClickListener {
+                callBack.onItemClick(item, binding.ivCover)
+            }
+            binding.cvContent.onLongClick {
+                callBack.onItemLongClick(item, binding.ivCover)
+            }
+        }
+
+        private fun loadGroupCover(item: BookGroup) = binding.run {
+
+            binding.ivCover.setImageDrawable(
+                ContextCompat.getDrawable(binding.root.context, R.drawable.bg_surface_variant)
+            )
+            binding.ivCover1.setImageDrawable(null)
+            binding.ivCover2.setImageDrawable(null)
+            binding.ivCover3.setImageDrawable(null)
+            binding.ivCover4.setImageDrawable(null)
+            if (!item.cover.isNullOrEmpty()) {
+                ivCover.load(item.cover)
+            } else {
+
+                val books = getBooksForGroup(item)
+
+                if (books.size > 0) binding.ivCover1.load(books[0].getDisplayCover())
+                if (books.size > 1) binding.ivCover2.load(books[1].getDisplayCover())
+                if (books.size > 2) binding.ivCover3.load(books[2].getDisplayCover())
+                if (books.size > 3) binding.ivCover4.load(books[3].getDisplayCover())
+            }
+        }
+
+    }
+
+}

+ 10 - 3
app/src/main/java/io/legato/kazusa/utils/ContextExtensions.kt

@@ -420,11 +420,18 @@ val Context.isDebuggable: Boolean
 
 val Context.dataStore by preferencesDataStore(name = "settings")
 
-val Context.bookshelfLayout: Int
+val Context.bookshelfLayoutMode: Int
     get() = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
-        AppConfig.bookshelfLayoutLandscape
+        AppConfig.bookshelfLayoutModeLandscape
     } else {
-        AppConfig.bookshelfLayoutPortrait
+        AppConfig.bookshelfLayoutModePortrait
+    }
+
+val Context.bookshelfLayoutGrid: Int
+    get() = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+        AppConfig.bookshelfLayoutGridLandscape
+    } else {
+        AppConfig.bookshelfLayoutGridPortrait
     }
 
 fun Context.themeColor(attr: Int): Int {

+ 8 - 0
app/src/main/res/drawable/bg_gradient_black.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <gradient
+        android:angle="90"
+        android:endColor="#00000000"
+        android:startColor="#CC000000"
+        android:type="linear" />
+</shape>

+ 37 - 33
app/src/main/res/layout/dialog_bookshelf_config.xml

@@ -80,55 +80,59 @@
 
             <com.google.android.material.chip.ChipGroup
                 android:id="@+id/chip_group_style"
-                android:layout_width="match_parent"
+                android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:layout_gravity="end"
                 android:checkableBehavior="single"
                 android:paddingBottom="8dp"
                 app:selectionRequired="true"
                 app:singleSelection="true" />
 
-            <LinearLayout
+            <TextView
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                android:paddingTop="8dp">
+                android:layout_gravity="center_vertical"
+                android:text="@string/sortview"
+                android:textSize="14sp"
+                android:textStyle="bold" />
 
-                <TextView
+            <com.google.android.material.chip.ChipGroup
+                android:id="@+id/chip_group_layout"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="end"
+                android:checkableBehavior="single"
+                app:selectionRequired="true"
+                app:singleSelection="true">
+
+                <com.google.android.material.chip.Chip
+                    android:id="@+id/chip_list"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:text="@string/sortview"
-                    android:textSize="14sp"
-                    android:textStyle="bold" />
+                    android:checkable="true"
+                    android:text="@string/list" />
 
-                <View
-                    android:layout_width="0dp"
-                    android:layout_height="match_parent"
-                    android:layout_weight="1" />
-
-                <com.google.android.material.chip.ChipGroup
-                    android:id="@+id/chip_group_layout"
+                <com.google.android.material.chip.Chip
+                    android:id="@+id/chip_grid"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    app:selectionRequired="true"
-                    app:singleSelection="true">
+                    android:checkable="true"
+                    android:text="@string/grid" />
 
-                    <com.google.android.material.chip.Chip
-                        android:id="@+id/chip_list"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:checkable="true"
-                        android:text="@string/list" />
-
-                    <com.google.android.material.chip.Chip
-                        android:id="@+id/chip_grid"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:checkable="true"
-                        android:text="@string/grid" />
-                </com.google.android.material.chip.ChipGroup>
-            </LinearLayout>
+                <com.google.android.material.chip.Chip
+                    android:id="@+id/chip_grid_compact"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:checkable="true"
+                    android:text="紧凑网格" />
 
+                <com.google.android.material.chip.Chip
+                    android:id="@+id/chip_grid_cover"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:checkable="true"
+                    android:text="封面网格" />
+            </com.google.android.material.chip.ChipGroup>
 
             <TextView
                 android:id="@+id/slider_text"

+ 92 - 0
app/src/main/res/layout/item_bookshelf_grid_compact.xml

@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?attr/selectableItemBackground"
+    app:strokeWidth="0dp">
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingHorizontal="6dp"
+        android:paddingTop="2dp"
+        android:paddingBottom="12dp">
+
+        <com.google.android.material.card.MaterialCardView
+            android:id="@+id/cv_content"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:clickable="true"
+            android:focusable="true"
+            app:cardBackgroundColor="@android:color/transparent"
+            app:cardCornerRadius="6dp"
+            app:layout_constraintTop_toTopOf="parent"
+            app:strokeWidth="0dp">
+
+            <io.legato.kazusa.ui.widget.image.CoverImageView
+                android:id="@+id/iv_cover"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:scaleType="centerCrop"
+                android:src="@drawable/image_cover_default"
+                android:transitionName="img_cover"
+                tools:ignore="ImageContrastCheck" />
+
+            <com.google.android.material.progressindicator.LinearProgressIndicator
+                android:id="@+id/rl_loading"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="top"
+                android:layout_marginHorizontal="12dp"
+                android:layout_marginVertical="4dp"
+                android:indeterminate="true"
+                android:visibility="invisible" />
+
+            <com.google.android.material.card.MaterialCardView
+                android:id="@+id/cd_unread"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="top|end"
+                android:layout_margin="4dp"
+                android:visibility="visible"
+                app:cardBackgroundColor="?attr/colorSecondaryVariant"
+                app:cardCornerRadius="4dp"
+                app:strokeWidth="0dp">
+
+                <TextView
+                    android:id="@+id/tv_unread"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:includeFontPadding="false"
+                    android:padding="4dp"
+                    android:textColor="?attr/colorSurface"
+                    android:textSize="10sp"
+                    android:textStyle="bold"
+                    tools:ignore="SmallSp"
+                    tools:text="5" />
+            </com.google.android.material.card.MaterialCardView>
+
+            <TextView
+                android:id="@+id/tv_name"
+                android:layout_width="match_parent"
+                android:layout_height="60dp"
+                android:layout_gravity="bottom"
+                android:background="@drawable/bg_gradient_black"
+                android:ellipsize="end"
+                android:gravity="bottom|start"
+                android:lines="2"
+                android:paddingHorizontal="6dp"
+                android:paddingVertical="6dp"
+                android:text="@string/book_name"
+                android:textColor="@android:color/white"
+                android:textSize="12sp"
+                android:textStyle="bold"
+                tools:ignore="RtlHardcoded" />
+        </com.google.android.material.card.MaterialCardView>
+
+    </FrameLayout>
+
+</com.google.android.material.card.MaterialCardView>
+

+ 159 - 0
app/src/main/res/layout/item_bookshelf_grid_compact_group.xml

@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?attr/selectableItemBackground"
+    app:strokeWidth="0dp">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:paddingTop="2dp"
+        android:paddingBottom="12dp">
+
+        <com.google.android.material.card.MaterialCardView
+            android:id="@+id/cv_content"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:clickable="true"
+            android:focusable="true"
+            app:cardCornerRadius="8dp"
+            app:layout_constraintTop_toTopOf="parent"
+            app:strokeWidth="0dp">
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+                <io.legato.kazusa.ui.widget.image.CoverImageView
+                    android:id="@+id/iv_cover"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:scaleType="centerCrop"
+                    android:src="?attr/colorSurfaceVariant"
+                    android:transitionName="img_cover"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent"
+                    tools:ignore="ImageContrastCheck" />
+
+                <com.google.android.material.progressindicator.LinearProgressIndicator
+                    android:id="@+id/rl_loading"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="top"
+                    android:layout_marginHorizontal="12dp"
+                    android:layout_marginVertical="4dp"
+                    android:indeterminate="true"
+                    android:visibility="invisible"
+                    app:layout_constraintEnd_toEndOf="@id/iv_cover"
+                    app:layout_constraintStart_toStartOf="@id/iv_cover"
+                    app:layout_constraintTop_toTopOf="@id/iv_cover" />
+
+                <com.google.android.material.card.MaterialCardView
+                    android:id="@+id/cd_unread"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_margin="4dp"
+                    android:visibility="visible"
+                    app:cardBackgroundColor="?attr/colorSecondaryVariant"
+                    app:cardCornerRadius="4dp"
+                    app:layout_constraintEnd_toEndOf="@id/iv_cover"
+                    app:layout_constraintTop_toTopOf="@id/iv_cover"
+                    app:strokeWidth="0dp">
+
+                    <TextView
+                        android:id="@+id/tv_unread"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:includeFontPadding="false"
+                        android:padding="4dp"
+                        android:textColor="?attr/colorSurface"
+                        android:textSize="10sp"
+                        android:textStyle="bold"
+                        tools:ignore="SmallSp"
+                        tools:text="5" />
+
+                </com.google.android.material.card.MaterialCardView>
+
+                <io.legato.kazusa.ui.widget.image.CoverImageView
+                    android:id="@+id/iv_cover1"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="4dp"
+                    android:layout_marginTop="4dp"
+                    android:layout_marginEnd="2dp"
+                    android:layout_marginBottom="2dp"
+                    app:layout_constraintBottom_toTopOf="@id/iv_cover3"
+                    app:layout_constraintEnd_toStartOf="@id/iv_cover2"
+                    app:layout_constraintStart_toStartOf="@id/iv_cover"
+                    app:layout_constraintTop_toTopOf="@id/iv_cover" />
+
+                <io.legato.kazusa.ui.widget.image.CoverImageView
+                    android:id="@+id/iv_cover2"
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_marginStart="2dp"
+                    android:layout_marginTop="4dp"
+                    android:layout_marginEnd="4dp"
+                    android:layout_marginBottom="2dp"
+                    app:layout_constraintBottom_toTopOf="@id/iv_cover4"
+                    app:layout_constraintEnd_toEndOf="@id/iv_cover"
+                    app:layout_constraintStart_toEndOf="@id/iv_cover1"
+                    app:layout_constraintTop_toTopOf="@id/iv_cover" />
+
+                <io.legato.kazusa.ui.widget.image.CoverImageView
+                    android:id="@+id/iv_cover3"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="4dp"
+                    android:layout_marginTop="2dp"
+                    android:layout_marginEnd="2dp"
+                    android:layout_marginBottom="4dp"
+                    app:layout_constraintBottom_toBottomOf="@id/iv_cover"
+                    app:layout_constraintEnd_toStartOf="@id/iv_cover4"
+                    app:layout_constraintStart_toStartOf="@id/iv_cover"
+                    app:layout_constraintTop_toBottomOf="@id/iv_cover1" />
+
+                <io.legato.kazusa.ui.widget.image.CoverImageView
+                    android:id="@+id/iv_cover4"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="2dp"
+                    android:layout_marginTop="2dp"
+                    android:layout_marginEnd="4dp"
+                    android:layout_marginBottom="4dp"
+                    app:layout_constraintBottom_toBottomOf="@id/iv_cover"
+                    app:layout_constraintEnd_toEndOf="@id/iv_cover"
+                    app:layout_constraintStart_toEndOf="@id/iv_cover3"
+                    app:layout_constraintTop_toBottomOf="@id/iv_cover2" />
+
+                <TextView
+                    android:id="@+id/tv_name"
+                    android:layout_width="0dp"
+                    android:layout_height="60dp"
+                    android:layout_gravity="bottom"
+                    android:background="@drawable/bg_gradient_black"
+                    android:ellipsize="end"
+                    android:gravity="bottom|start"
+                    android:lines="2"
+                    android:paddingHorizontal="6dp"
+                    android:paddingVertical="6dp"
+                    android:text="@string/book_name"
+                    android:textColor="@android:color/white"
+                    android:textSize="12sp"
+                    android:textStyle="bold"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    tools:ignore="RtlHardcoded" />
+
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+        </com.google.android.material.card.MaterialCardView>
+
+    </LinearLayout>
+</com.google.android.material.card.MaterialCardView>