Merge pull request #9207 from cern1710/list-view-alt-alt-implementation

undefined
This commit is contained in:
Stypox 2022-10-27 22:48:03 +02:00 committed by GitHub
commit 3cef7f3201
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 462 additions and 243 deletions

View File

@ -5,7 +5,6 @@ import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingSc
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
@ -31,6 +30,7 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.SuperScrollLayoutManager; import org.schabi.newpipe.views.SuperScrollLayoutManager;
import java.util.List; import java.util.List;
@ -476,15 +476,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
} }
protected boolean isGridLayout() { protected boolean isGridLayout() {
final String listMode = PreferenceManager.getDefaultSharedPreferences(activity) return ThemeHelper.shouldUseGridLayout(activity);
.getString(getString(R.string.list_view_mode_key),
getString(R.string.list_view_mode_value));
if ("auto".equals(listMode)) {
final Configuration configuration = getResources().getConfiguration();
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
} else {
return "grid".equals(listMode);
}
} }
} }

View File

@ -22,7 +22,6 @@ import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.xwray.groupie.Group import com.xwray.groupie.Group
import com.xwray.groupie.GroupAdapter import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Item
import com.xwray.groupie.Section import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.GroupieViewHolder import com.xwray.groupie.viewbinding.GroupieViewHolder
import icepick.State import icepick.State
@ -43,11 +42,13 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialog import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialog
import org.schabi.newpipe.local.subscription.item.ChannelItem import org.schabi.newpipe.local.subscription.item.ChannelItem
import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem
import org.schabi.newpipe.local.subscription.item.FeedGroupAddItem import org.schabi.newpipe.local.subscription.item.FeedGroupAddNewGridItem
import org.schabi.newpipe.local.subscription.item.FeedGroupAddNewItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardGridItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem import org.schabi.newpipe.local.subscription.item.GroupsHeader
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM import org.schabi.newpipe.local.subscription.item.Header
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
@ -74,9 +75,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private val disposables: CompositeDisposable = CompositeDisposable() private val disposables: CompositeDisposable = CompositeDisposable()
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>() private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
private val feedGroupsSection = Section() private lateinit var carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>
private var feedGroupsCarousel: FeedGroupCarouselItem? = null private lateinit var feedGroupsCarousel: FeedGroupCarouselItem
private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem private lateinit var feedGroupsSortMenuItem: GroupsHeader
private val subscriptionsSection = Section() private val subscriptionsSection = Section()
private val requestExportLauncher = private val requestExportLauncher =
@ -90,7 +91,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
@State @State
@JvmField @JvmField
var feedGroupsListState: Parcelable? = null var feedGroupsCarouselState: Parcelable? = null
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
@ -100,11 +101,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
// Fragment LifeCycle // Fragment LifeCycle
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupInitialLayout()
}
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
subscriptionManager = SubscriptionManager(requireContext()) subscriptionManager = SubscriptionManager(requireContext())
@ -117,7 +113,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState() itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState()
feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState() feedGroupsCarouselState = feedGroupsCarousel.onSaveInstanceState()
} }
override fun onDestroy() { override fun onDestroy() {
@ -184,7 +180,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
menuItem: MenuItem, menuItem: MenuItem,
onClick: Runnable onClick: Runnable
): MenuItem { ): MenuItem {
menuItem.setOnMenuItemClickListener { _ -> menuItem.setOnMenuItemClickListener {
onClick.run() onClick.run()
true true
} }
@ -245,51 +241,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
// Fragment Views // Fragment Views
// //////////////////////////////////////////////////////////////////////// // ////////////////////////////////////////////////////////////////////////
private fun setupInitialLayout() {
Section().apply {
val carouselAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
carouselAdapter.add(FeedGroupCardItem(-1, getString(R.string.all), FeedGroupIcon.RSS))
carouselAdapter.add(feedGroupsSection)
carouselAdapter.add(FeedGroupAddItem())
carouselAdapter.setOnItemClickListener { item, _ ->
listenerFeedGroups.selected(item)
}
carouselAdapter.setOnItemLongClickListener { item, _ ->
if (item is FeedGroupCardItem) {
if (item.groupId == FeedGroupEntity.GROUP_ALL_ID) {
return@setOnItemLongClickListener false
}
}
listenerFeedGroups.held(item)
return@setOnItemLongClickListener true
}
feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter)
feedGroupsSortMenuItem = HeaderWithMenuItem(
getString(R.string.feed_groups_header_title),
R.drawable.ic_sort,
menuItemOnClickListener = ::openReorderDialog
)
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
groupAdapter.add(this)
}
subscriptionsSection.setPlaceholder(EmptyPlaceholderItem())
subscriptionsSection.setHideWhenEmpty(true)
groupAdapter.add(
Section(
HeaderWithMenuItem(
getString(R.string.tab_subscriptions)
),
listOf(subscriptionsSection)
)
)
}
override fun initViews(rootView: View, savedInstanceState: Bundle?) { override fun initViews(rootView: View, savedInstanceState: Bundle?) {
super.initViews(rootView, savedInstanceState) super.initViews(rootView, savedInstanceState)
_binding = FragmentSubscriptionBinding.bind(rootView) _binding = FragmentSubscriptionBinding.bind(rootView)
@ -299,10 +250,83 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
spanSizeLookup = groupAdapter.spanSizeLookup spanSizeLookup = groupAdapter.spanSizeLookup
} }
binding.itemsList.adapter = groupAdapter binding.itemsList.adapter = groupAdapter
binding.itemsList.itemAnimator = null
viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java) viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java]
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) } viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let(this::handleFeedGroups) } viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let(this::handleFeedGroups) }
setupInitialLayout()
}
private fun setupInitialLayout() {
Section().apply {
carouselAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
carouselAdapter.setOnItemClickListener { item, _ ->
when (item) {
is FeedGroupCardItem ->
NavigationHelper.openFeedFragment(fm, item.groupId, item.name)
is FeedGroupCardGridItem ->
NavigationHelper.openFeedFragment(fm, item.groupId, item.name)
is FeedGroupAddNewItem ->
FeedGroupDialog.newInstance().show(fm, null)
is FeedGroupAddNewGridItem ->
FeedGroupDialog.newInstance().show(fm, null)
}
}
carouselAdapter.setOnItemLongClickListener { item, _ ->
if ((
item is FeedGroupCardItem &&
item.groupId == FeedGroupEntity.GROUP_ALL_ID
) ||
(
item is FeedGroupCardGridItem &&
item.groupId == FeedGroupEntity.GROUP_ALL_ID
)
) {
return@setOnItemLongClickListener false
}
when (item) {
is FeedGroupCardItem ->
FeedGroupDialog.newInstance(item.groupId).show(fm, null)
is FeedGroupCardGridItem ->
FeedGroupDialog.newInstance(item.groupId).show(fm, null)
}
return@setOnItemLongClickListener true
}
feedGroupsCarousel = FeedGroupCarouselItem(
carouselAdapter = carouselAdapter,
listViewMode = viewModel.getListViewMode()
)
feedGroupsSortMenuItem = GroupsHeader(
title = getString(R.string.feed_groups_header_title),
onSortClicked = ::openReorderDialog,
onToggleListViewModeClicked = ::toggleListViewMode,
listViewMode = viewModel.getListViewMode(),
)
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
groupAdapter.clear()
groupAdapter.add(this)
}
subscriptionsSection.setPlaceholder(EmptyPlaceholderItem())
subscriptionsSection.setHideWhenEmpty(true)
groupAdapter.add(
Section(
Header(getString(R.string.tab_subscriptions)),
listOf(subscriptionsSection)
)
)
}
private fun toggleListViewMode() {
viewModel.setListViewMode(!viewModel.getListViewMode())
} }
private fun showLongTapDialog(selectedItem: ChannelInfoItem) { private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
@ -346,21 +370,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun doInitialLoadLogic() = Unit override fun doInitialLoadLogic() = Unit
override fun startLoading(forceLoad: Boolean) = Unit override fun startLoading(forceLoad: Boolean) = Unit
private val listenerFeedGroups = object : OnClickGesture<Item<*>> {
override fun selected(selectedItem: Item<*>?) {
when (selectedItem) {
is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name)
is FeedGroupAddItem -> FeedGroupDialog.newInstance().show(fm, null)
}
}
override fun held(selectedItem: Item<*>?) {
when (selectedItem) {
is FeedGroupCardItem -> FeedGroupDialog.newInstance(selectedItem.groupId).show(fm, null)
}
}
}
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem> { private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem> {
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment( override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
fm, fm,
@ -403,15 +412,39 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
} }
private fun handleFeedGroups(groups: List<Group>) { private fun handleFeedGroups(groups: List<Group>) {
feedGroupsSection.update(groups) val listViewMode = viewModel.getListViewMode()
if (feedGroupsListState != null) { if (feedGroupsCarouselState != null) {
feedGroupsCarousel?.onRestoreInstanceState(feedGroupsListState) feedGroupsCarousel.onRestoreInstanceState(feedGroupsCarouselState)
feedGroupsListState = null feedGroupsCarouselState = null
} }
feedGroupsSortMenuItem.showMenuItem = groups.size > 1 feedGroupsCarousel.listViewMode = listViewMode
binding.itemsList.post { feedGroupsSortMenuItem.notifyChanged(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM) } feedGroupsSortMenuItem.showSortButton = groups.size > 1
feedGroupsSortMenuItem.listViewMode = listViewMode
binding.itemsList.post {
if (context == null) {
// since this part was posted to the next UI cycle, the fragment might have been
// removed in the meantime
return@post
}
feedGroupsCarousel.notifyChanged(FeedGroupCarouselItem.PAYLOAD_UPDATE_LIST_VIEW_MODE)
feedGroupsSortMenuItem.notifyChanged(GroupsHeader.PAYLOAD_UPDATE_ICONS)
// update items here to prevent flickering
carouselAdapter.apply {
clear()
if (listViewMode) {
add(FeedGroupAddNewItem())
add(FeedGroupCardItem(-1, getString(R.string.all), FeedGroupIcon.RSS))
} else {
add(FeedGroupAddNewGridItem())
add(FeedGroupCardGridItem(-1, getString(R.string.all), FeedGroupIcon.RSS))
}
addAll(groups)
}
}
} }
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////

View File

@ -5,25 +5,42 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.xwray.groupie.Group import com.xwray.groupie.Group
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.subscription.item.ChannelItem import org.schabi.newpipe.local.subscription.item.ChannelItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardGridItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import org.schabi.newpipe.util.ThemeHelper
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class SubscriptionViewModel(application: Application) : AndroidViewModel(application) { class SubscriptionViewModel(application: Application) : AndroidViewModel(application) {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application) private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)
private var subscriptionManager = SubscriptionManager(application) private var subscriptionManager = SubscriptionManager(application)
// true -> list view, false -> grid view
private val listViewMode = BehaviorProcessor.createDefault(
!ThemeHelper.shouldUseGridLayout(application)
)
private val listViewModeFlowable = listViewMode.distinctUntilChanged()
private val mutableStateLiveData = MutableLiveData<SubscriptionState>() private val mutableStateLiveData = MutableLiveData<SubscriptionState>()
private val mutableFeedGroupsLiveData = MutableLiveData<List<Group>>() private val mutableFeedGroupsLiveData = MutableLiveData<List<Group>>()
val stateLiveData: LiveData<SubscriptionState> = mutableStateLiveData val stateLiveData: LiveData<SubscriptionState> = mutableStateLiveData
val feedGroupsLiveData: LiveData<List<Group>> = mutableFeedGroupsLiveData val feedGroupsLiveData: LiveData<List<Group>> = mutableFeedGroupsLiveData
private var feedGroupItemsDisposable = feedDatabaseManager.groups() private var feedGroupItemsDisposable = Flowable
.combineLatest(
feedDatabaseManager.groups(),
listViewModeFlowable,
::Pair
)
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.map { it.map(::FeedGroupCardItem) } .map { (feedGroups, listViewMode) ->
feedGroups.map(if (listViewMode) ::FeedGroupCardItem else ::FeedGroupCardGridItem)
}
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe( .subscribe(
{ mutableFeedGroupsLiveData.postValue(it) }, { mutableFeedGroupsLiveData.postValue(it) },
@ -45,6 +62,14 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica
feedGroupItemsDisposable.dispose() feedGroupItemsDisposable.dispose()
} }
fun setListViewMode(newListViewMode: Boolean) {
listViewMode.onNext(newListViewMode)
}
fun getListViewMode(): Boolean {
return listViewMode.value ?: true
}
sealed class SubscriptionState { sealed class SubscriptionState {
data class LoadedState(val subscriptions: List<Group>) : SubscriptionState() data class LoadedState(val subscriptions: List<Group>) : SubscriptionState()
data class ErrorState(val error: Throwable? = null) : SubscriptionState() data class ErrorState(val error: Throwable? = null) : SubscriptionState()

View File

@ -1,35 +0,0 @@
package org.schabi.newpipe.local.subscription.decoration
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.schabi.newpipe.R
class FeedGroupCarouselDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val marginStartEnd: Int
private val marginTopBottom: Int
private val marginBetweenItems: Int
init {
with(context.resources) {
marginStartEnd = getDimensionPixelOffset(R.dimen.feed_group_carousel_start_end_margin)
marginTopBottom = getDimensionPixelOffset(R.dimen.feed_group_carousel_top_bottom_margin)
marginBetweenItems = getDimensionPixelOffset(R.dimen.feed_group_carousel_between_items_margin)
}
}
override fun getItemOffsets(outRect: Rect, child: View, parent: RecyclerView, state: RecyclerView.State) {
val childAdapterPosition = parent.getChildAdapterPosition(child)
val childAdapterCount = parent.adapter?.itemCount ?: 0
outRect.set(marginBetweenItems, marginTopBottom, 0, marginTopBottom)
if (childAdapterPosition == 0) {
outRect.left = marginStartEnd
} else if (childAdapterPosition == childAdapterCount - 1) {
outRect.right = marginStartEnd
}
}
}

View File

@ -0,0 +1,14 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FeedGroupAddNewGridItemBinding
class FeedGroupAddNewGridItem : BindableItem<FeedGroupAddNewGridItemBinding>() {
override fun getLayout(): Int = R.layout.feed_group_add_new_grid_item
override fun initializeViewBinding(view: View) = FeedGroupAddNewGridItemBinding.bind(view)
override fun bind(viewBinding: FeedGroupAddNewGridItemBinding, position: Int) {
// this is a static item, nothing to do here
}
}

View File

@ -5,8 +5,10 @@ import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FeedGroupAddNewItemBinding import org.schabi.newpipe.databinding.FeedGroupAddNewItemBinding
class FeedGroupAddItem : BindableItem<FeedGroupAddNewItemBinding>() { class FeedGroupAddNewItem : BindableItem<FeedGroupAddNewItemBinding>() {
override fun getLayout(): Int = R.layout.feed_group_add_new_item override fun getLayout(): Int = R.layout.feed_group_add_new_item
override fun bind(viewBinding: FeedGroupAddNewItemBinding, position: Int) {}
override fun initializeViewBinding(view: View) = FeedGroupAddNewItemBinding.bind(view) override fun initializeViewBinding(view: View) = FeedGroupAddNewItemBinding.bind(view)
override fun bind(viewBinding: FeedGroupAddNewItemBinding, position: Int) {
// this is a static item, nothing to do here
}
} }

View File

@ -0,0 +1,32 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.FeedGroupCardGridItemBinding
import org.schabi.newpipe.local.subscription.FeedGroupIcon
data class FeedGroupCardGridItem(
val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
val name: String,
val icon: FeedGroupIcon,
) : BindableItem<FeedGroupCardGridItemBinding>() {
constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon)
override fun getId(): Long {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> super.getId()
else -> groupId
}
}
override fun getLayout(): Int = R.layout.feed_group_card_grid_item
override fun bind(viewBinding: FeedGroupCardGridItemBinding, position: Int) {
viewBinding.title.text = name
viewBinding.icon.setImageResource(icon.getDrawableRes())
}
override fun initializeViewBinding(view: View) = FeedGroupCardGridItemBinding.bind(view)
}

View File

@ -1,60 +1,85 @@
package org.schabi.newpipe.local.subscription.item package org.schabi.newpipe.local.subscription.item
import android.content.Context
import android.os.Parcelable import android.os.Parcelable
import android.view.View import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupAdapter import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder import com.xwray.groupie.viewbinding.GroupieViewHolder
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FeedItemCarouselBinding import org.schabi.newpipe.databinding.FeedItemCarouselBinding
import org.schabi.newpipe.local.subscription.decoration.FeedGroupCarouselDecoration import org.schabi.newpipe.util.DeviceUtils
import java.lang.Integer.max
class FeedGroupCarouselItem( class FeedGroupCarouselItem(
context: Context, private val carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>,
private val carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>> var listViewMode: Boolean
) : BindableItem<FeedItemCarouselBinding>() { ) : BindableItem<FeedItemCarouselBinding>() {
private val feedGroupCarouselDecoration = FeedGroupCarouselDecoration(context) companion object {
const val PAYLOAD_UPDATE_LIST_VIEW_MODE = 2
}
private var linearLayoutManager: LinearLayoutManager? = null private var carouselLayoutManager: LinearLayoutManager? = null
private var listState: Parcelable? = null private var listState: Parcelable? = null
override fun getLayout() = R.layout.feed_item_carousel override fun getLayout() = R.layout.feed_item_carousel
fun onSaveInstanceState(): Parcelable? { fun onSaveInstanceState(): Parcelable? {
listState = linearLayoutManager?.onSaveInstanceState() listState = carouselLayoutManager?.onSaveInstanceState()
return listState return listState
} }
fun onRestoreInstanceState(state: Parcelable?) { fun onRestoreInstanceState(state: Parcelable?) {
linearLayoutManager?.onRestoreInstanceState(state) carouselLayoutManager?.onRestoreInstanceState(state)
listState = state listState = state
} }
override fun initializeViewBinding(view: View): FeedItemCarouselBinding { override fun initializeViewBinding(view: View): FeedItemCarouselBinding {
val viewHolder = FeedItemCarouselBinding.bind(view) val viewBinding = FeedItemCarouselBinding.bind(view)
updateViewMode(viewBinding)
return viewBinding
}
linearLayoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false) override fun bind(
viewBinding: FeedItemCarouselBinding,
viewHolder.recyclerView.apply { position: Int,
layoutManager = linearLayoutManager payloads: MutableList<Any>
adapter = carouselAdapter ) {
addItemDecoration(feedGroupCarouselDecoration) if (payloads.contains(PAYLOAD_UPDATE_LIST_VIEW_MODE)) {
updateViewMode(viewBinding)
return
} }
return viewHolder super.bind(viewBinding, position, payloads)
} }
override fun bind(viewBinding: FeedItemCarouselBinding, position: Int) { override fun bind(viewBinding: FeedItemCarouselBinding, position: Int) {
viewBinding.recyclerView.apply { adapter = carouselAdapter } viewBinding.recyclerView.apply { adapter = carouselAdapter }
linearLayoutManager?.onRestoreInstanceState(listState) carouselLayoutManager?.onRestoreInstanceState(listState)
} }
override fun unbind(viewHolder: GroupieViewHolder<FeedItemCarouselBinding>) { override fun unbind(viewHolder: GroupieViewHolder<FeedItemCarouselBinding>) {
super.unbind(viewHolder) super.unbind(viewHolder)
listState = carouselLayoutManager?.onSaveInstanceState()
}
listState = linearLayoutManager?.onSaveInstanceState() private fun updateViewMode(viewBinding: FeedItemCarouselBinding) {
viewBinding.recyclerView.apply { adapter = carouselAdapter }
val context = viewBinding.root.context
carouselLayoutManager = if (listViewMode) {
LinearLayoutManager(context)
} else {
GridLayoutManager(
context,
max(1, viewBinding.recyclerView.width / DeviceUtils.dpToPx(112, context))
)
}
viewBinding.recyclerView.apply {
layoutManager = carouselLayoutManager
adapter = carouselAdapter
}
} }
} }

View File

@ -0,0 +1,50 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import androidx.core.view.isVisible
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.SubscriptionGroupsHeaderBinding
class GroupsHeader(
private val title: String,
private val onSortClicked: () -> Unit,
private val onToggleListViewModeClicked: () -> Unit,
var showSortButton: Boolean = true,
var listViewMode: Boolean = true
) : BindableItem<SubscriptionGroupsHeaderBinding>() {
companion object {
const val PAYLOAD_UPDATE_ICONS = 1
}
override fun getLayout(): Int = R.layout.subscription_groups_header
override fun bind(
viewBinding: SubscriptionGroupsHeaderBinding,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.contains(PAYLOAD_UPDATE_ICONS)) {
updateIcons(viewBinding)
return
}
super.bind(viewBinding, position, payloads)
}
override fun bind(viewBinding: SubscriptionGroupsHeaderBinding, position: Int) {
viewBinding.headerTitle.text = title
viewBinding.headerSort.setOnClickListener { onSortClicked() }
viewBinding.headerToggleViewMode.setOnClickListener { onToggleListViewModeClicked() }
updateIcons(viewBinding)
}
override fun initializeViewBinding(view: View) = SubscriptionGroupsHeaderBinding.bind(view)
private fun updateIcons(viewBinding: SubscriptionGroupsHeaderBinding) {
viewBinding.headerToggleViewMode.setImageResource(
if (listViewMode) R.drawable.ic_apps else R.drawable.ic_list
)
viewBinding.headerSort.isVisible = showSortButton
}
}

View File

@ -0,0 +1,17 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.SubscriptionHeaderBinding
class Header(private val title: String) : BindableItem<SubscriptionHeaderBinding>() {
override fun getLayout(): Int = R.layout.subscription_header
override fun bind(viewBinding: SubscriptionHeaderBinding, position: Int) {
viewBinding.root.text = title
}
override fun initializeViewBinding(view: View) = SubscriptionHeaderBinding.bind(view)
}

View File

@ -1,50 +0,0 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import android.view.View.OnClickListener
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.HeaderWithMenuItemBinding
class HeaderWithMenuItem(
val title: String,
@DrawableRes val itemIcon: Int = 0,
var showMenuItem: Boolean = true,
private val onClickListener: (() -> Unit)? = null,
private val menuItemOnClickListener: (() -> Unit)? = null
) : BindableItem<HeaderWithMenuItemBinding>() {
companion object {
const val PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM = 1
}
override fun getLayout(): Int = R.layout.header_with_menu_item
override fun bind(viewBinding: HeaderWithMenuItemBinding, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM)) {
updateMenuItemVisibility(viewBinding)
return
}
super.bind(viewBinding, position, payloads)
}
override fun bind(viewBinding: HeaderWithMenuItemBinding, position: Int) {
viewBinding.headerTitle.text = title
viewBinding.headerMenuItem.setImageResource(itemIcon)
val listener = onClickListener?.let { OnClickListener { onClickListener.invoke() } }
viewBinding.root.setOnClickListener(listener)
val menuItemListener = menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } }
viewBinding.headerMenuItem.setOnClickListener(menuItemListener)
updateMenuItemVisibility(viewBinding)
}
override fun initializeViewBinding(view: View) = HeaderWithMenuItemBinding.bind(view)
private fun updateMenuItemVisibility(viewBinding: HeaderWithMenuItemBinding) {
viewBinding.headerMenuItem.isVisible = showMenuItem
}
}

View File

@ -349,7 +349,7 @@ public final class ThemeHelper {
return false; return false;
} else if (listMode.equals(context.getString(R.string.list_view_mode_grid_key))) { } else if (listMode.equals(context.getString(R.string.list_view_mode_grid_key))) {
return true; return true;
} else { } else /* listMode.equals("auto") */ {
final Configuration configuration = context.getResources().getConfiguration(); final Configuration configuration = context.getResources().getConfiguration();
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE); && configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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:layout_gravity="center_vertical"
android:layout_margin="4dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
app:cardBackgroundColor="?attr/card_item_background_color">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/dashed_border"
android:gravity="center"
android:orientation="vertical"
android:padding="4dp">
<ImageView
android:id="@+id/icon"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:padding="4dp"
android:scaleType="centerInside"
android:src="@drawable/ic_add"
tools:ignore="ContentDescription" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:text="@string/feed_create_new_group_button_title"
android:textAllCaps="true"
android:textColor="?attr/colorAccent"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -2,9 +2,10 @@
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="64dp" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_margin="4dp"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
@ -14,15 +15,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/dashed_border" android:background="?attr/dashed_border"
android:gravity="center" android:orientation="horizontal">
android:orientation="vertical"
android:padding="4dp">
<ImageView <ImageView
android:id="@+id/icon" android:id="@+id/icon"
android:layout_width="14dp" android:layout_width="48dp"
android:layout_height="14dp" android:layout_height="48dp"
android:layout_gravity="center" android:layout_gravity="center"
android:padding="8dp"
android:scaleType="centerInside" android:scaleType="centerInside"
android:src="@drawable/ic_add" android:src="@drawable/ic_add"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
@ -31,15 +31,14 @@
android:id="@+id/title" android:id="@+id/title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2dp" android:layout_gravity="center_vertical"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center"
android:maxLines="1" android:maxLines="1"
android:padding="10dp"
android:text="@string/feed_create_new_group_button_title" android:text="@string/feed_create_new_group_button_title"
android:textAllCaps="true" android:textAllCaps="true"
android:textColor="?attr/colorAccent" android:textColor="?attr/colorAccent"
android:textSize="10sp" android:textSize="14sp"
android:textStyle="bold" android:textStyle="bold" />
tools:ignore="SmallSp" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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:layout_gravity="center"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:layout_margin="4dp"
app:cardBackgroundColor="?attr/card_item_background_color">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="2dp">
<ImageView
android:id="@+id/icon"
android:layout_width="36dp"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_weight="1"
android:padding="4dp"
android:scaleType="fitCenter"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_fastfood" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/card_item_contrast_color"
android:ellipsize="end"
android:gravity="center"
android:maxLines="2"
android:padding="2dp"
android:textAllCaps="false"
android:textColor="?attr/colorAccent"
android:textSize="14sp"
android:textStyle="bold"
tools:text="ALL" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -2,9 +2,10 @@
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="64dp" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_margin="4dp"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
@ -12,36 +13,34 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="horizontal">
android:paddingTop="2dp">
<ImageView <ImageView
android:id="@+id/icon" android:id="@+id/icon"
android:layout_width="18dp" android:layout_width="48dp"
android:layout_height="0dp" android:layout_height="48dp"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_weight="1" android:padding="8dp"
android:paddingTop="2dp" android:scaleType="centerInside"
android:paddingBottom="2dp"
android:scaleType="fitCenter"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:src="@drawable/ic_fastfood" /> tools:src="@drawable/ic_fastfood" />
<org.schabi.newpipe.views.NewPipeTextView <org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/title" android:id="@+id/title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:background="?attr/card_item_contrast_color" android:background="?attr/card_item_contrast_color"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center" android:gravity="center_vertical"
android:maxLines="2" android:maxLines="1"
android:padding="2dp" android:padding="10dp"
android:textAllCaps="false" android:textAllCaps="false"
android:textColor="?attr/colorAccent" android:textColor="?attr/colorAccent"
android:textSize="10sp" android:textSize="14sp"
android:textStyle="bold" android:textStyle="bold"
tools:ignore="SmallSp" tools:text="All" />
tools:text="ALL" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View File

@ -3,4 +3,5 @@
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="4dp"
android:scrollbars="none" /> android:scrollbars="none" />

View File

@ -2,11 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:paddingLeft="16dp"
android:paddingTop="12dp"
android:paddingRight="16dp"
android:paddingBottom="12dp">
<org.schabi.newpipe.views.NewPipeTextView <org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/header_title" android:id="@+id/header_title"
@ -17,16 +13,29 @@
android:gravity="start|center_vertical" android:gravity="start|center_vertical"
android:maxLines="2" android:maxLines="2"
android:minHeight="24dp" android:minHeight="24dp"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp" android:textSize="16sp"
android:textStyle="bold" android:textStyle="bold"
tools:text="Header" /> tools:text="Header" />
<ImageButton <ImageButton
android:id="@+id/header_menu_item" android:id="@+id/header_toggle_view_mode"
android:layout_width="24dp" android:layout_width="48dp"
android:layout_height="24dp" android:layout_height="48dp"
android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
tools:src="@drawable/ic_bookmark" /> android:contentDescription="@string/list_view_mode"
android:padding="8dp"
tools:src="@drawable/ic_apps" />
<ImageButton
android:id="@+id/header_sort"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="4dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/sort"
android:padding="8dp"
android:src="@drawable/ic_sort" />
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<org.schabi.newpipe.views.NewPipeTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:paddingLeft="16dp"
android:paddingTop="12dp"
android:paddingRight="16dp"
android:paddingBottom="12dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold"
tools:text="Header" />

View File

@ -125,7 +125,7 @@
<!-- Feed Groups dimensions--> <!-- Feed Groups dimensions-->
<dimen name="feed_group_carousel_start_end_margin">12dp</dimen> <dimen name="feed_group_carousel_start_end_margin">12dp</dimen>
<dimen name="feed_group_carousel_top_bottom_margin">2dp</dimen> <dimen name="feed_group_carousel_top_bottom_margin">6dp</dimen>
<dimen name="feed_group_carousel_between_items_margin">4dp</dimen> <dimen name="feed_group_carousel_between_items_margin">4dp</dimen>
<dimen name="search_suggestion_text_size">16sp</dimen> <dimen name="search_suggestion_text_size">16sp</dimen>

View File

@ -754,4 +754,5 @@
<string name="unknown_quality">Unknown quality</string> <string name="unknown_quality">Unknown quality</string>
<string name="feed_toggle_show_future_items">Show future items</string> <string name="feed_toggle_show_future_items">Show future items</string>
<string name="feed_toggle_hide_future_items">Hide future items</string> <string name="feed_toggle_hide_future_items">Hide future items</string>
<string name="sort">Sort</string>
</resources> </resources>