Added the ability to view the library as a list (#394)

* Added in the ability to view the library as a list

* reverted LibraryAdapter and renamed libraryToggleViewEvent to LibraryToggleViewEvent for consistency

* removed LibraryToggleViewEvent and directly subscribed to option change

* fixed the toggleView subscription

* Made the library list item layout more compliant with material design

* Changed unread text style and removed background
This commit is contained in:
Josh 2016-07-27 10:37:36 -05:00 committed by Bram van de Kerkhof
parent 74e3d387eb
commit f21a030cf8
16 changed files with 313 additions and 44 deletions

View File

@ -164,6 +164,7 @@ dependencies {
compile 'net.xpece.android:support-preference:0.8.1' compile 'net.xpece.android:support-preference:0.8.1'
compile 'me.zhanghai.android.systemuihelper:library:1.0.0' compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
compile 'org.adw.library:discrete-seekbar:1.0.1' compile 'org.adw.library:discrete-seekbar:1.0.1'
compile 'de.hdodenhof:circleimageview:2.1.0'
// Tests // Tests
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'

View File

@ -92,4 +92,6 @@ class PreferenceKeys(context: Context) {
fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId" fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId"
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
} }

View File

@ -126,6 +126,8 @@ class PreferencesHelper(context: Context) {
fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet()) fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet())
fun libraryAsList() = rxPrefs.getBoolean(keys.libraryAsList, false)
fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false) fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false)
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false) fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)

View File

@ -84,11 +84,19 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryFragment) :
* @return a new view holder for a manga. * @return a new view holder for a manga.
*/ */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryHolder {
//depending on preferences, display a list or display a grid
if(parent.id == R.id.library_list) {
val view = parent.inflate(R.layout.item_library_list)
return LibraryListHolder(view, this, fragment)
} else {
val view = parent.inflate(R.layout.item_catalogue_grid).apply { val view = parent.inflate(R.layout.item_catalogue_grid).apply {
card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight) card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM) gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
} }
return LibraryHolder(view, this, fragment) return LibraryGridHolder(view, this, fragment)
}
} }
/** /**

View File

@ -7,17 +7,22 @@ import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AnimationUtils
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
import eu.kanade.tachiyomi.ui.manga.MangaActivity import eu.kanade.tachiyomi.ui.manga.MangaActivity
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.fragment_catalogue.*
import kotlinx.android.synthetic.main.fragment_library.*
import kotlinx.android.synthetic.main.fragment_library_category.* import kotlinx.android.synthetic.main.fragment_library_category.*
import rx.Subscription import rx.Subscription
import uy.kohesive.injekt.injectLazy
/** /**
* Fragment containing the library manga for a certain category. * Fragment containing the library manga for a certain category.
@ -25,6 +30,11 @@ import rx.Subscription
*/ */
class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemClickListener { class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemClickListener {
/**
* Preferences.
*/
val preferences: PreferencesHelper by injectLazy()
/** /**
* Adapter to hold the manga in this category. * Adapter to hold the manga in this category.
*/ */
@ -46,11 +56,21 @@ class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemCli
*/ */
private var numColumnsSubscription: Subscription? = null private var numColumnsSubscription: Subscription? = null
/**
* subscription to view toggle
*/
private var toggleViewSubscription: Subscription? = null
/** /**
* Subscription of the library search. * Subscription of the library search.
*/ */
private var searchSubscription: Subscription? = null private var searchSubscription: Subscription? = null
/**
* display mode
*/
private var displayAsList: Boolean = false;
companion object { companion object {
/** /**
* Key to save and restore [position] from a [Bundle]. * Key to save and restore [position] from a [Bundle].
@ -66,19 +86,29 @@ class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemCli
fun newInstance(position: Int): LibraryCategoryFragment { fun newInstance(position: Int): LibraryCategoryFragment {
val fragment = LibraryCategoryFragment() val fragment = LibraryCategoryFragment()
fragment.position = position fragment.position = position
return fragment return fragment
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_library_category, container, false) return inflater.inflate(R.layout.fragment_library_category, container, false)
} }
override fun onViewCreated(view: View, savedState: Bundle?) { override fun onViewCreated(view: View, savedState: Bundle?) {
adapter = LibraryCategoryAdapter(this) adapter = LibraryCategoryAdapter(this)
//set up grid
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.adapter = adapter recycler.adapter = adapter
//set up list
library_list.setHasFixedSize(true)
library_list.adapter = adapter
library_list.layoutManager = LinearLayoutManager(activity)
if (libraryFragment.actionMode != null) { if (libraryFragment.actionMode != null) {
setSelectionMode(FlexibleAdapter.MODE_MULTI) setSelectionMode(FlexibleAdapter.MODE_MULTI)
} }
@ -94,6 +124,17 @@ class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemCli
adapter.updateDataSet() adapter.updateDataSet()
} }
toggleViewSubscription = preferences.libraryAsList().asObservable().subscribe {onViewModeChange(it)}
if(libraryPresenter.displayAsList != displayAsList) {
library_switcher.showNext()
displayAsList = libraryPresenter.displayAsList
}
library_switcher.inAnimation = AnimationUtils.loadAnimation(activity, android.R.anim.fade_in)
library_switcher.outAnimation = AnimationUtils.loadAnimation(activity, android.R.anim.fade_out)
if (savedState != null) { if (savedState != null) {
position = savedState.getInt(POSITION_KEY) position = savedState.getInt(POSITION_KEY)
adapter.onRestoreInstanceState(savedState) adapter.onRestoreInstanceState(savedState)
@ -129,13 +170,17 @@ class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemCli
override fun onDestroyView() { override fun onDestroyView() {
numColumnsSubscription?.unsubscribe() numColumnsSubscription?.unsubscribe()
searchSubscription?.unsubscribe() searchSubscription?.unsubscribe()
toggleViewSubscription?.unsubscribe()
super.onDestroyView() super.onDestroyView()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
libraryMangaSubscription = libraryPresenter.libraryMangaSubject libraryMangaSubscription = libraryPresenter.libraryMangaSubject
.subscribe { onNextLibraryManga(it) } .subscribe { onNextLibraryManga(it) }
} }
override fun onPause() { override fun onPause() {
@ -211,6 +256,7 @@ class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemCli
startActivity(intent) startActivity(intent)
} }
/** /**
* Toggles the selection for a manga. * Toggles the selection for a manga.
* *
@ -262,6 +308,15 @@ class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemCli
} }
} }
fun onViewModeChange(isList: Boolean) {
//do nothing if the display does not need to change
if(isList == displayAsList) return
//else change view and display mode
library_switcher.showNext()
displayAsList = isList
}
/** /**
* Property to get the library fragment. * Property to get the library fragment.
*/ */

View File

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
import eu.kanade.tachiyomi.ui.category.CategoryActivity import eu.kanade.tachiyomi.ui.category.CategoryActivity
@ -22,6 +23,7 @@ import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_library.* import kotlinx.android.synthetic.main.fragment_library.*
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
/** /**
@ -37,6 +39,11 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
lateinit var adapter: LibraryAdapter lateinit var adapter: LibraryAdapter
private set private set
/**
* Preferences.
*/
val preferences: PreferencesHelper by injectLazy()
/** /**
* TabLayout of the categories. * TabLayout of the categories.
*/ */
@ -53,6 +60,11 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
*/ */
private var query: String? = null private var query: String? = null
/**
* Display mode of the library (list or grid mode).
*/
private var displayMode: MenuItem? = null
/** /**
* Action mode for manga selection. * Action mode for manga selection.
*/ */
@ -178,6 +190,18 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
return true return true
} }
}) })
//set the icon for the display mode button
displayMode = menu.findItem(R.id.action_library_display_mode).apply {
val icon = if (preferences.libraryAsList().getOrDefault())
R.drawable.ic_view_module_white_24dp
else
R.drawable.ic_view_list_white_24dp
setIcon(icon)
}
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -208,6 +232,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
// Apply filter // Apply filter
onFilterCheckboxChanged() onFilterCheckboxChanged()
} }
R.id.action_library_display_mode -> swapDisplayMode()
R.id.action_update_library -> { R.id.action_update_library -> {
LibraryUpdateService.start(activity, true) LibraryUpdateService.start(activity, true)
} }
@ -231,6 +256,23 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
activity.supportInvalidateOptionsMenu() activity.supportInvalidateOptionsMenu()
} }
/**
* swap display mode
*/
private fun swapDisplayMode() {
presenter.swapDisplayMode()
val isListMode = presenter.displayAsList
val icon = if (isListMode)
R.drawable.ic_view_module_white_24dp
else
R.drawable.ic_view_list_white_24dp
displayMode?.setIcon(icon)
}
/** /**
* Updates the query. * Updates the query.
* *

View File

@ -0,0 +1,49 @@
package eu.kanade.tachiyomi.ui.library
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
/**
* 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_catalogue_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 LibraryGridHolder(private val view: View,
private val adapter: LibraryCategoryAdapter,
listener: FlexibleViewHolder.OnListItemClickListener)
: LibraryHolder(view, adapter, listener) {
/**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga.
*
* @param manga the manga to bind.
*/
override fun onSetValues(manga: Manga) {
// Update the title of the manga.
view.title.text = manga.title
// Update the unread count and its visibility.
with(view.unread_text) {
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
text = manga.unread.toString()
}
// Update the cover.
Glide.clear(view.thumbnail)
Glide.with(view.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.into(view.thumbnail)
}
}

View File

@ -8,18 +8,14 @@ import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import kotlinx.android.synthetic.main.item_catalogue_grid.view.* import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Generic class used to hold the displayed data of a manga in the library.
* All the elements from the layout file "item_catalogue_grid" are available in this class.
*
* @param view the inflated view for this holder. * @param view the inflated view for this holder.
* @param adapter the adapter handling this holder. * @param adapter the adapter handling this holder.
* @param listener a listener to react to single tap and long tap events. * @param listener a listener to react to the single tap and long tap events.
* @constructor creates a new library holder.
*/ */
class LibraryHolder(private val view: View,
private val adapter: LibraryCategoryAdapter, abstract class LibraryHolder(private val view: View, adapter: LibraryCategoryAdapter, listener: FlexibleViewHolder.OnListItemClickListener)
listener: FlexibleViewHolder.OnListItemClickListener) : FlexibleViewHolder(view, adapter, listener) {
: FlexibleViewHolder(view, adapter, listener) {
/** /**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
@ -27,23 +23,6 @@ class LibraryHolder(private val view: View,
* *
* @param manga the manga to bind. * @param manga the manga to bind.
*/ */
fun onSetValues(manga: Manga) { abstract fun onSetValues(manga: Manga)
// Update the title of the manga.
view.title.text = manga.title
// Update the unread count and its visibility.
with(view.unread_text) {
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
text = manga.unread.toString()
}
// Update the cover.
Glide.clear(view.thumbnail)
Glide.with(view.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.into(view.thumbnail)
}
} }

View File

@ -0,0 +1,53 @@
package eu.kanade.tachiyomi.ui.library
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import kotlinx.android.synthetic.main.item_library_list.view.*
/**
* 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_library_list" 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 LibraryListHolder(private val view: View,
private val adapter: LibraryCategoryAdapter,
listener: FlexibleViewHolder.OnListItemClickListener)
: LibraryHolder(view, adapter, listener) {
/**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga.
*
* @param manga the manga to bind.
*/
override fun onSetValues(manga: Manga) {
// Update the title of the manga.
view.title.text = manga.title
// Update the unread count and its visibility.
with(view.unread_text) {
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
text = manga.unread.toString()
}
// Update the cover.
Glide.clear(view.thumbnail)
Glide.with(view.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.dontAnimate()
.into(view.thumbnail)
}
}

View File

@ -71,6 +71,12 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
*/ */
val downloadManager: DownloadManager by injectLazy() val downloadManager: DownloadManager by injectLazy()
/**
* display the library as a list?
*/
var displayAsList: Boolean = false
private set
companion object { companion object {
/** /**
* Id of the restartable that listens for library updates. * Id of the restartable that listens for library updates.
@ -89,6 +95,18 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
start(GET_LIBRARY) start(GET_LIBRARY)
} }
add(preferences.libraryAsList().asObservable().subscribe{setDisplayMode(it)})
}
/**
* Sets the display mode
*
* @param asList display as list or not
*/
fun setDisplayMode(asList: Boolean) {
displayAsList = asList
} }
/** /**
@ -285,4 +303,12 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
return false return false
} }
/**
* Changes the active display mode.
*/
fun swapDisplayMode() {
var currentMode: Boolean = displayAsList
preferences.libraryAsList().set(!displayAsList)
}
} }

View File

@ -6,8 +6,6 @@
<android.support.v4.view.ViewPager <android.support.v4.view.ViewPager
android:id="@+id/view_pager" android:id="@+id/view_pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"/>
</android.support.v4.view.ViewPager>
</LinearLayout> </LinearLayout>

View File

@ -9,6 +9,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ViewSwitcher
android:id="@+id/library_switcher"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<eu.kanade.tachiyomi.widget.AutofitRecyclerView <eu.kanade.tachiyomi.widget.AutofitRecyclerView
android:id="@+id/recycler" android:id="@+id/recycler"
style="@style/Theme.Widget.GridView" style="@style/Theme.Widget.GridView"
@ -17,6 +23,13 @@
android:columnWidth="140dp" android:columnWidth="140dp"
tools:listitem="@layout/item_catalogue_grid"/> tools:listitem="@layout/item_catalogue_grid"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/library_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_library_list"/>
</ViewSwitcher>
</android.support.v4.widget.SwipeRefreshLayout> </android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout> </FrameLayout>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?attr/selectable_list_drawable">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/thumbnail"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/icon"
android:layout_gravity="center_vertical|left"
android:paddingLeft="6dp"/>
<TextView
android:id="@+id/title"
style="@style/TextAppearance.Regular.Body1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="38dp"
android:text="Sample name"/>
<TextView
android:id="@+id/unread_text"
style="@style/TextAppearance.Regular.Caption"
android:textColor="@color/material_grey_500"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|right"
android:paddingRight="9dp"
android:visibility="gone"/>
</FrameLayout>

View File

@ -29,6 +29,11 @@
app:showAsAction="collapseActionView|ifRoom" app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="android.support.v7.widget.SearchView" /> app:actionViewClass="android.support.v7.widget.SearchView" />
<item
android:id="@+id/action_library_display_mode"
android:title="Display Mode"
app:showAsAction="always"/>
<item <item
android:id="@+id/action_update_library" android:id="@+id/action_update_library"
android:title="@string/action_update_library" android:title="@string/action_update_library"

View File

@ -9,6 +9,7 @@
<string name="pref_category_about_key">pref_category_about_key</string> <string name="pref_category_about_key">pref_category_about_key</string>
<string name="pref_category_sources_key">pref_category_sources_key</string> <string name="pref_category_sources_key">pref_category_sources_key</string>
<string name="pref_display_library_as_list">pref_display_library_as_list</string>
<string name="pref_library_columns_dialog_key">pref_library_columns_dialog_key</string> <string name="pref_library_columns_dialog_key">pref_library_columns_dialog_key</string>
<string name="pref_library_columns_portrait_key">pref_library_columns_portrait_key</string> <string name="pref_library_columns_portrait_key">pref_library_columns_portrait_key</string>
<string name="pref_library_columns_landscape_key">pref_library_columns_landscape_key</string> <string name="pref_library_columns_landscape_key">pref_library_columns_landscape_key</string>