Эх сурвалжийг харах

Comfortable Grid (#3238)

* Comfortable Grid

* Add requested changes

* Add more requested changes
jobobby04 4 жил өмнө
parent
commit
52e82b3548
17 өөрчлөгдсөн 383 нэмэгдсэн , 72 устгасан
  1. 2 2
      app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
  2. 4 0
      app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceValues.kt
  3. 3 2
      app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
  4. 21 16
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt
  5. 4 14
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt
  6. 56 0
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt
  7. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt
  8. 29 13
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt
  9. 2 1
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
  10. 68 0
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt
  11. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt
  12. 27 12
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
  13. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
  14. 16 5
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt
  15. 130 0
      app/src/main/res/layout/source_comfortable_grid_item.xml
  16. 15 2
      app/src/main/res/menu/source_browse.xml
  17. 2 1
      app/src/main/res/values/strings.xml

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt

@@ -73,7 +73,7 @@ object PreferenceKeys {
 
     const val lastUsedCategory = "last_used_category"
 
-    const val catalogueAsList = "pref_display_catalogue_as_list"
+    const val catalogueDisplayMode = "pref_display_catalogue_display_mode"
 
     const val enabledLanguages = "source_languages"
 
@@ -131,7 +131,7 @@ object PreferenceKeys {
 
     const val downloadNewCategories = "download_new_categories"
 
-    const val libraryAsList = "pref_display_library_as_list"
+    const val libraryDisplayMode = "pref_display_library_display_mode"
 
     const val lang = "app_language"
 

+ 4 - 0
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceValues.kt

@@ -15,4 +15,8 @@ object PreferenceValues {
     const val THEME_DARK_DEFAULT = "default"
     const val THEME_DARK_BLUE = "blue"
     const val THEME_DARK_AMOLED = "amoled"
+
+    const val DISPLAY_COMPACT_GRID = 0
+    const val DISPLAY_LIST = 1
+    const val DISPLAY_COMFORTABLE_GRID = 2
 }

+ 3 - 2
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt

@@ -7,6 +7,7 @@ import androidx.preference.PreferenceManager
 import com.tfcporciuncula.flow.FlowSharedPreferences
 import com.tfcporciuncula.flow.Preference
 import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferenceValues.DISPLAY_COMPACT_GRID
 import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
 import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
 import eu.kanade.tachiyomi.data.track.TrackService
@@ -138,7 +139,7 @@ class PreferencesHelper(val context: Context) {
 
     fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
 
-    fun catalogueAsList() = flowPrefs.getBoolean(Keys.catalogueAsList, false)
+    fun catalogueDisplayMode() = flowPrefs.getInt(Keys.catalogueDisplayMode, DISPLAY_COMPACT_GRID)
 
     fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("en", Locale.getDefault().language))
 
@@ -184,7 +185,7 @@ class PreferencesHelper(val context: Context) {
 
     fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
 
-    fun libraryAsList() = flowPrefs.getBoolean(Keys.libraryAsList, false)
+    fun libraryDisplayMode() = flowPrefs.getInt(Keys.libraryDisplayMode, DISPLAY_COMPACT_GRID)
 
     fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
 

+ 21 - 16
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt

@@ -22,6 +22,9 @@ import eu.davidea.flexibleadapter.items.IFlexible
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Category
 import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.preference.PreferenceValues.DISPLAY_COMFORTABLE_GRID
+import eu.kanade.tachiyomi.data.preference.PreferenceValues.DISPLAY_COMPACT_GRID
+import eu.kanade.tachiyomi.data.preference.PreferenceValues.DISPLAY_LIST
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.data.preference.asImmediateFlow
 import eu.kanade.tachiyomi.databinding.SourceControllerBinding
@@ -187,7 +190,7 @@ open class BrowseSourceController(bundle: Bundle) :
             binding.catalogueView.removeView(oldRecycler)
         }
 
-        val recycler = if (presenter.isListMode) {
+        val recycler = if (preferences.catalogueDisplayMode().get() == DISPLAY_LIST) {
             RecyclerView(view.context).apply {
                 id = R.id.recycler
                 layoutManager = LinearLayoutManager(context)
@@ -205,7 +208,7 @@ open class BrowseSourceController(bundle: Bundle) :
                 (layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
                     override fun getSpanSize(position: Int): Int {
                         return when (adapter?.getItemViewType(position)) {
-                            R.layout.source_grid_item, null -> 1
+                            R.layout.source_grid_item, R.layout.source_comfortable_grid_item, null -> 1
                             else -> spanCount
                         }
                     }
@@ -266,15 +269,13 @@ open class BrowseSourceController(bundle: Bundle) :
             }
         )
 
-        // Show next display mode
-        menu.findItem(R.id.action_display_mode).apply {
-            val icon = if (presenter.isListMode) {
-                R.drawable.ic_view_module_24dp
-            } else {
-                R.drawable.ic_view_list_24dp
-            }
-            setIcon(icon)
+        val displayItem = when (preferences.catalogueDisplayMode().get()) {
+            DISPLAY_COMPACT_GRID -> R.id.action_compact_grid
+            DISPLAY_LIST -> R.id.action_list
+            DISPLAY_COMFORTABLE_GRID -> R.id.action_comfortable_grid
+            else -> throw NotImplementedError("Unimplemented display")
         }
+        menu.findItem(displayItem).isChecked = true
     }
 
     override fun onPrepareOptionsMenu(menu: Menu) {
@@ -290,7 +291,9 @@ open class BrowseSourceController(bundle: Bundle) :
     override fun onOptionsItemSelected(item: MenuItem): Boolean {
         when (item.itemId) {
             R.id.action_search -> expandActionViewFromInteraction = true
-            R.id.action_display_mode -> swapDisplayMode()
+            R.id.action_compact_grid -> setDisplayMode(DISPLAY_COMPACT_GRID)
+            R.id.action_list -> setDisplayMode(DISPLAY_LIST)
+            R.id.action_comfortable_grid -> setDisplayMode(DISPLAY_COMFORTABLE_GRID)
             R.id.action_open_in_web_view -> openInWebView()
             R.id.action_local_source_help -> openLocalSourceHelpGuide()
         }
@@ -433,17 +436,19 @@ open class BrowseSourceController(bundle: Bundle) :
     }
 
     /**
-     * Swaps the current display mode.
+     * Sets the current display mode.
+     *
+     * @param mode the mode to change to
      */
-    private fun swapDisplayMode() {
+    private fun setDisplayMode(mode: Int) {
         val view = view ?: return
         val adapter = adapter ?: return
 
-        presenter.swapDisplayMode()
-        val isListMode = presenter.isListMode
+        preferences.catalogueDisplayMode().set(mode)
+        presenter.refreshDisplayMode()
         activity?.invalidateOptionsMenu()
         setupRecycler(view)
-        if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
+        if (mode == DISPLAY_LIST || !view.context.connectivityManager.isActiveNetworkMetered) {
             // Initialize mangas if going to grid view or if over wifi when going to list view
             val mangas = (0 until adapter.itemCount).mapNotNull {
                 (adapter.getItem(it) as? SourceItem)?.manga

+ 4 - 14
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt

@@ -87,12 +87,6 @@ open class BrowseSourcePresenter(
      */
     private val mangaDetailSubject = PublishSubject.create<List<Manga>>()
 
-    /**
-     * Whether the view is in list mode or not.
-     */
-    var isListMode: Boolean = false
-        private set
-
     /**
      * Subscription for the pager.
      */
@@ -119,7 +113,6 @@ open class BrowseSourcePresenter(
             query = savedState.getString(::query.name, "")
         }
 
-        isListMode = prefs.catalogueAsList().get()
         restartPager()
     }
 
@@ -145,7 +138,7 @@ open class BrowseSourcePresenter(
 
         val sourceId = source.id
 
-        val catalogueAsList = prefs.catalogueAsList()
+        val catalogueDisplayMode = prefs.catalogueDisplayMode()
 
         // Prepare the pager.
         pagerSubscription?.let { remove(it) }
@@ -153,7 +146,7 @@ open class BrowseSourcePresenter(
             .observeOn(Schedulers.io())
             .map { pair -> pair.first to pair.second.map { networkToLocalManga(it, sourceId) } }
             .doOnNext { initializeMangas(it.second) }
-            .map { pair -> pair.first to pair.second.map { SourceItem(it, catalogueAsList) } }
+            .map { pair -> pair.first to pair.second.map { SourceItem(it, catalogueDisplayMode) } }
             .observeOn(AndroidSchedulers.mainThread())
             .subscribeReplay(
                 { view, (page, mangas) ->
@@ -273,12 +266,9 @@ open class BrowseSourcePresenter(
     }
 
     /**
-     * Changes the active display mode.
+     * Refreshes the active display mode.
      */
-    fun swapDisplayMode() {
-        val mode = !isListMode
-        prefs.catalogueAsList().set(mode)
-        isListMode = mode
+    fun refreshDisplayMode() {
         subscribeToMangaInitializer()
     }
 

+ 56 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt

@@ -0,0 +1,56 @@
+package eu.kanade.tachiyomi.ui.browse.source.browse
+
+import android.view.View
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.glide.GlideApp
+import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
+import eu.kanade.tachiyomi.widget.StateImageViewTarget
+import kotlinx.android.synthetic.main.source_comfortable_grid_item.card
+import kotlinx.android.synthetic.main.source_comfortable_grid_item.progress
+import kotlinx.android.synthetic.main.source_comfortable_grid_item.thumbnail
+import kotlinx.android.synthetic.main.source_comfortable_grid_item.title
+
+/**
+ * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
+ * All the elements from the layout file "item_source_grid" are available in this class.
+ *
+ * @param view the inflated view for this holder.
+ * @param adapter the adapter handling this holder.
+ * @constructor creates a new catalogue holder.
+ */
+class SourceComfortableGridHolder(private val view: View, private val adapter: FlexibleAdapter<*>) :
+    SourceGridHolder(view, adapter) {
+
+    /**
+     * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
+     * holder with the given manga.
+     *
+     * @param manga the manga to bind.
+     */
+    override fun onSetValues(manga: Manga) {
+        // Set manga title
+        title.text = manga.title
+
+        // Set alpha of thumbnail.
+        thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
+
+        setImage(manga)
+    }
+
+    override fun setImage(manga: Manga) {
+        // Setting this via XML doesn't work
+        card.clipToOutline = true
+
+        GlideApp.with(view.context).clear(thumbnail)
+        if (!manga.thumbnail_url.isNullOrEmpty()) {
+            GlideApp.with(view.context)
+                .load(manga.toMangaThumbnail())
+                .diskCacheStrategy(DiskCacheStrategy.DATA)
+                .centerCrop()
+                .placeholder(android.R.color.transparent)
+                .into(StateImageViewTarget(thumbnail, progress))
+        }
+    }
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt

@@ -20,7 +20,7 @@ import kotlinx.android.synthetic.main.source_grid_item.title
  * @param adapter the adapter handling this holder.
  * @constructor creates a new catalogue holder.
  */
-class SourceGridHolder(private val view: View, private val adapter: FlexibleAdapter<*>) :
+open class SourceGridHolder(private val view: View, private val adapter: FlexibleAdapter<*>) :
     SourceHolder(view, adapter) {
 
     /**

+ 29 - 13
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt

@@ -4,6 +4,7 @@ import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.widget.FrameLayout
+import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.recyclerview.widget.RecyclerView
 import com.tfcporciuncula.flow.Preference
 import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -11,18 +12,20 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 import eu.davidea.flexibleadapter.items.IFlexible
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.preference.PreferenceValues.DISPLAY_COMPACT_GRID
+import eu.kanade.tachiyomi.data.preference.PreferenceValues.DISPLAY_LIST
 import eu.kanade.tachiyomi.widget.AutofitRecyclerView
 import kotlinx.android.synthetic.main.source_grid_item.view.card
 import kotlinx.android.synthetic.main.source_grid_item.view.gradient
 
-class SourceItem(val manga: Manga, private val catalogueAsList: Preference<Boolean>) :
+class SourceItem(val manga: Manga, private val catalogueDisplayMode: Preference<Int>) :
     AbstractFlexibleItem<SourceHolder>() {
 
     override fun getLayoutRes(): Int {
-        return if (catalogueAsList.get()) {
-            R.layout.source_list_item
-        } else {
-            R.layout.source_grid_item
+        return when (catalogueDisplayMode.get()) {
+            DISPLAY_COMPACT_GRID -> R.layout.source_grid_item
+            DISPLAY_LIST -> R.layout.source_list_item
+            else -> R.layout.source_comfortable_grid_item
         }
     }
 
@@ -32,15 +35,28 @@ class SourceItem(val manga: Manga, private val catalogueAsList: Preference<Boole
     ): SourceHolder {
         val parent = adapter.recyclerView
         return if (parent is AutofitRecyclerView) {
-            view.apply {
-                card.layoutParams = FrameLayout.LayoutParams(
-                    MATCH_PARENT, parent.itemWidth / 3 * 4
-                )
-                gradient.layoutParams = FrameLayout.LayoutParams(
-                    MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM
-                )
+            val coverHeight = parent.itemWidth / 3 * 4
+            if (catalogueDisplayMode.get() == DISPLAY_COMPACT_GRID) {
+                view.apply {
+                    card.layoutParams = FrameLayout.LayoutParams(
+                        MATCH_PARENT, coverHeight
+                    )
+                    gradient.layoutParams = FrameLayout.LayoutParams(
+                        MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM
+                    )
+                }
+                SourceGridHolder(view, adapter)
+            } else {
+                view.apply {
+                    card.layoutParams = ConstraintLayout.LayoutParams(
+                        MATCH_PARENT, coverHeight
+                    )
+                    gradient.layoutParams = FrameLayout.LayoutParams(
+                        MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM
+                    )
+                }
+                SourceComfortableGridHolder(view, adapter)
             }
-            SourceGridHolder(view, adapter)
         } else {
             SourceListHolder(view, adapter)
         }

+ 2 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt

@@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Category
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.data.preference.PreferenceValues.DISPLAY_LIST
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.util.lang.plusAssign
 import eu.kanade.tachiyomi.util.system.toast
@@ -72,7 +73,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
     fun onCreate(controller: LibraryController) {
         this.controller = controller
 
-        recycler = if (preferences.libraryAsList().get()) {
+        recycler = if (preferences.libraryDisplayMode().get() == DISPLAY_LIST) {
             (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
                 layoutManager = LinearLayoutManager(context)
             }

+ 68 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt

@@ -0,0 +1,68 @@
+package eu.kanade.tachiyomi.ui.library
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.kanade.tachiyomi.data.glide.GlideApp
+import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
+import eu.kanade.tachiyomi.util.isLocal
+import eu.kanade.tachiyomi.util.view.visibleIf
+import kotlinx.android.synthetic.main.source_comfortable_grid_item.card
+import kotlinx.android.synthetic.main.source_comfortable_grid_item.download_text
+import kotlinx.android.synthetic.main.source_comfortable_grid_item.local_text
+import kotlinx.android.synthetic.main.source_comfortable_grid_item.thumbnail
+import kotlinx.android.synthetic.main.source_comfortable_grid_item.title
+import kotlinx.android.synthetic.main.source_comfortable_grid_item.unread_text
+
+/**
+ * Class used to hold the displayed data of a manga in the library, like the cover or the title.
+ * All the elements from the layout file "item_source_grid" are available in this class.
+ *
+ * @param view the inflated view for this holder.
+ * @param adapter the adapter handling this holder.
+ * @param listener a listener to react to single tap and long tap events.
+ * @constructor creates a new library holder.
+ */
+class LibraryComfortableGridHolder(
+    private val view: View,
+    adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
+) : LibraryGridHolder(view, adapter) {
+
+    /**
+     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
+     * holder with the given manga.
+     *
+     * @param item the manga item to bind.
+     */
+    override fun onSetValues(item: LibraryItem) {
+        // Update the title of the manga.
+        title.text = item.manga.title
+
+        // Update the unread count and its visibility.
+        with(unread_text) {
+            visibleIf { item.unreadCount > 0 }
+            text = item.unreadCount.toString()
+        }
+        // Update the download count and its visibility.
+        with(download_text) {
+            visibleIf { item.downloadCount > 0 }
+            text = item.downloadCount.toString()
+        }
+        // set local visibility if its local manga
+        local_text.visibleIf { item.manga.isLocal() }
+
+        // Setting this via XML doesn't work
+        card.clipToOutline = true
+
+        // Update the cover.
+        GlideApp.with(view.context).clear(thumbnail)
+        GlideApp.with(view.context)
+            .load(item.manga.toMangaThumbnail())
+            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
+            .centerCrop()
+            .dontAnimate()
+            .into(thumbnail)
+    }
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt

@@ -23,7 +23,7 @@ import kotlinx.android.synthetic.main.source_grid_item.unread_text
  * @param listener a listener to react to single tap and long tap events.
  * @constructor creates a new library holder.
  */
-class LibraryGridHolder(
+open class LibraryGridHolder(
     private val view: View,
     private val adapter: FlexibleAdapter<*>
 ) : LibraryHolder(view, adapter) {

+ 27 - 12
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt

@@ -4,6 +4,7 @@ import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.widget.FrameLayout
+import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.recyclerview.widget.RecyclerView
 import com.tfcporciuncula.flow.Preference
 import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -12,6 +13,8 @@ import eu.davidea.flexibleadapter.items.IFilterable
 import eu.davidea.flexibleadapter.items.IFlexible
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.LibraryManga
+import eu.kanade.tachiyomi.data.preference.PreferenceValues.DISPLAY_COMPACT_GRID
+import eu.kanade.tachiyomi.data.preference.PreferenceValues.DISPLAY_LIST
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.widget.AutofitRecyclerView
 import kotlinx.android.synthetic.main.source_grid_item.view.card
@@ -19,7 +22,7 @@ import kotlinx.android.synthetic.main.source_grid_item.view.gradient
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
-class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference<Boolean>) :
+class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Preference<Int>) :
     AbstractFlexibleItem<LibraryHolder>(), IFilterable<String> {
 
     private val sourceManager: SourceManager = Injekt.get()
@@ -28,24 +31,36 @@ class LibraryItem(val manga: LibraryManga, private val libraryAsList: Preference
     var unreadCount = -1
 
     override fun getLayoutRes(): Int {
-        return if (libraryAsList.get()) {
-            R.layout.source_list_item
-        } else {
-            R.layout.source_grid_item
+        return when (libraryDisplayMode.get()) {
+            DISPLAY_COMPACT_GRID -> R.layout.source_grid_item
+            DISPLAY_LIST -> R.layout.source_list_item
+            else -> R.layout.source_comfortable_grid_item
         }
     }
 
     override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
         val parent = adapter.recyclerView
         return if (parent is AutofitRecyclerView) {
-            view.apply {
-                val coverHeight = parent.itemWidth / 3 * 4
-                card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
-                gradient.layoutParams = FrameLayout.LayoutParams(
-                    MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM
-                )
+            val coverHeight = parent.itemWidth / 3 * 4
+            if (libraryDisplayMode.get() == DISPLAY_COMPACT_GRID) {
+                view.apply {
+                    card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
+                    gradient.layoutParams = FrameLayout.LayoutParams(
+                        MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM
+                    )
+                }
+                LibraryGridHolder(view, adapter)
+            } else {
+                view.apply {
+                    card.layoutParams = ConstraintLayout.LayoutParams(
+                        MATCH_PARENT, coverHeight
+                    )
+                    gradient.layoutParams = FrameLayout.LayoutParams(
+                        MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM
+                    )
+                }
+                LibraryComfortableGridHolder(view, adapter)
             }
-            LibraryGridHolder(view, adapter)
         } else {
             LibraryListHolder(view, adapter)
         }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt

@@ -266,10 +266,10 @@ class LibraryPresenter(
      * value.
      */
     private fun getLibraryMangasObservable(): Observable<LibraryMap> {
-        val libraryAsList = preferences.libraryAsList()
+        val libraryDisplayMode = preferences.libraryDisplayMode()
         return db.getLibraryMangas().asRxObservable()
             .map { list ->
-                list.map { LibraryItem(it, libraryAsList) }.groupBy { it.manga.category }
+                list.map { LibraryItem(it, libraryDisplayMode) }.groupBy { it.manga.category }
             }
     }
 

+ 16 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt

@@ -5,6 +5,9 @@ import android.content.Context
 import android.util.AttributeSet
 import android.view.View
 import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferenceValues.DISPLAY_COMFORTABLE_GRID
+import eu.kanade.tachiyomi.data.preference.PreferenceValues.DISPLAY_COMPACT_GRID
+import eu.kanade.tachiyomi.data.preference.PreferenceValues.DISPLAY_LIST
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.widget.ExtendedNavigationView
 import eu.kanade.tachiyomi.widget.TabbedBottomSheetDialog
@@ -183,16 +186,18 @@ class LibrarySettingsSheet(
         inner class DisplayGroup : Group {
 
             private val grid = Item.Radio(R.string.action_display_grid, this)
+            private val comfortableGrid = Item.Radio(R.string.action_display_comfortable_grid, this)
             private val list = Item.Radio(R.string.action_display_list, this)
 
             override val header = null
-            override val items = listOf(grid, list)
+            override val items = listOf(grid, comfortableGrid, list)
             override val footer = null
 
             override fun initModels() {
-                val asList = preferences.libraryAsList().get()
-                grid.checked = !asList
-                list.checked = asList
+                val mode = preferences.libraryDisplayMode().get()
+                grid.checked = mode == DISPLAY_COMPACT_GRID
+                list.checked = mode == DISPLAY_LIST
+                comfortableGrid.checked = mode == DISPLAY_COMFORTABLE_GRID
             }
 
             override fun onItemClicked(item: Item) {
@@ -202,7 +207,13 @@ class LibrarySettingsSheet(
                 item.group.items.forEach { (it as Item.Radio).checked = false }
                 item.checked = true
 
-                preferences.libraryAsList().set(item == list)
+                preferences.libraryDisplayMode().set(
+                    when (item) {
+                        grid -> DISPLAY_COMPACT_GRID
+                        list -> DISPLAY_LIST
+                        else -> DISPLAY_COMFORTABLE_GRID
+                    }
+                )
 
                 item.group.items.forEach { adapter.notifyItemChanged(it) }
             }

+ 130 - 0
app/src/main/res/layout/source_comfortable_grid_item.xml

@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout 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="@drawable/library_item_selector"
+    android:padding="4dp">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/rounded_rectangle">
+
+        <FrameLayout
+            android:id="@+id/card"
+            android:layout_width="wrap_content"
+            android:layout_height="220dp"
+            android:background="@drawable/rounded_rectangle"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent">
+
+            <ImageView
+                android:id="@+id/thumbnail"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="?attr/colorSurface"
+                tools:ignore="ContentDescription"
+                tools:src="@mipmap/ic_launcher" />
+
+            <View
+                android:id="@+id/gradient"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="bottom"
+                android:background="@drawable/gradient_shape" />
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                tools:layout_editor_absoluteX="7dp"
+                tools:layout_editor_absoluteY="7dp">
+
+                <TextView
+                    android:id="@+id/unread_text"
+                    style="@style/TextAppearance.Regular.Caption"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="4dp"
+                    android:layout_marginTop="4dp"
+                    android:background="@color/colorAccentDark"
+                    android:paddingStart="3dp"
+                    android:paddingTop="1dp"
+                    android:paddingEnd="3dp"
+                    android:paddingBottom="1dp"
+                    android:textColor="@color/md_white_1000"
+                    android:visibility="gone"
+                    app:layout_constraintStart_toEndOf="@+id/download_text"
+                    app:layout_constraintTop_toTopOf="parent"
+                    tools:text="120"
+                    tools:visibility="visible" />
+
+                <TextView
+                    android:id="@+id/download_text"
+                    style="@style/TextAppearance.Regular.Caption"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="4dp"
+                    android:layout_marginTop="4dp"
+                    android:background="@color/md_red_500"
+                    android:paddingStart="3dp"
+                    android:paddingTop="1dp"
+                    android:paddingEnd="3dp"
+                    android:paddingBottom="1dp"
+                    android:textColor="@color/md_white_1000"
+                    android:visibility="gone"
+                    app:layout_constraintStart_toEndOf="@+id/local_text"
+                    app:layout_constraintTop_toTopOf="parent"
+                    tools:text="120"
+                    tools:visibility="visible" />
+
+                <TextView
+                    android:id="@+id/local_text"
+                    style="@style/TextAppearance.Regular.Caption"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="4dp"
+                    android:layout_marginTop="4dp"
+                    android:background="@color/md_teal_500"
+                    android:paddingStart="3dp"
+                    android:paddingTop="1dp"
+                    android:paddingEnd="3dp"
+                    android:paddingBottom="1dp"
+                    android:text="@string/local_source_badge"
+                    android:textColor="@color/md_white_1000"
+                    android:visibility="gone"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent"
+                    tools:visibility="visible" />
+
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+            <ProgressBar
+                android:id="@+id/progress"
+                style="?android:attr/progressBarStyleSmall"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:visibility="gone" />
+
+        </FrameLayout>
+
+        <TextView
+            android:id="@+id/title"
+            style="@style/TextAppearance.Regular.Body1"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="bottom|start|end"
+            android:ellipsize="end"
+            android:fontFamily="@font/ptsans_narrow_bold"
+            android:lineSpacingExtra="-4dp"
+            android:maxLines="2"
+            android:padding="8dp"
+            android:shadowColor="@color/textColorPrimaryLight"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/card"
+            tools:text="Sample name" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</FrameLayout>

+ 15 - 2
app/src/main/res/menu/source_browse.xml

@@ -10,11 +10,24 @@
         app:showAsAction="collapseActionView|ifRoom" />
 
     <item
-        android:id="@+id/action_display_mode"
         android:icon="@drawable/ic_view_module_24dp"
         android:title="@string/action_display_mode"
         app:iconTint="?attr/colorOnPrimary"
-        app:showAsAction="ifRoom" />
+        app:showAsAction="ifRoom">
+        <menu>
+            <group android:checkableBehavior="single">
+                <item
+                    android:id="@+id/action_compact_grid"
+                    android:title="@string/action_display_grid" />
+                <item
+                    android:id="@+id/action_comfortable_grid"
+                    android:title="@string/action_display_comfortable_grid" />
+                <item
+                    android:id="@+id/action_list"
+                    android:title="@string/action_display_list" />
+            </group>
+        </menu>
+    </item>
 
     <item
         android:id="@+id/action_open_in_web_view"

+ 2 - 1
app/src/main/res/values/strings.xml

@@ -81,8 +81,9 @@
     <string name="action_migrate">Migrate</string>
     <string name="action_display_mode">Display mode</string>
     <string name="action_display">Display</string>
-    <string name="action_display_grid">Grid</string>
+    <string name="action_display_grid">Compact grid</string>
     <string name="action_display_list">List</string>
+    <string name="action_display_comfortable_grid">Comfortable grid</string>
     <string name="action_display_download_badge">Download badges</string>
     <string name="action_display_unread_badge">Unread badges</string>
     <string name="action_hide">Hide</string>