Skip to content

Commit 3202aca

Browse files
committed
feat: move subscriptions into a separate bottom sheet
1 parent 77f67b0 commit 3202aca

8 files changed

Lines changed: 133 additions & 164 deletions

File tree

app/src/main/java/com/github/libretube/ui/adapters/SubscriptionChannelAdapter.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.github.libretube.api.obj.Subscription
88
import com.github.libretube.constants.IntentData
99
import com.github.libretube.databinding.ChannelSubscriptionRowBinding
1010
import com.github.libretube.extensions.toID
11+
import com.github.libretube.helpers.ContextHelper
1112
import com.github.libretube.helpers.ImageHelper
1213
import com.github.libretube.helpers.NavigationHelper
1314
import com.github.libretube.ui.adapters.callbacks.DiffUtilItemCallback
@@ -45,7 +46,8 @@ class SubscriptionChannelAdapter :
4546
IntentData.channelName to subscription.name,
4647
IntentData.isSubscribed to true
4748
)
48-
channelOptionsSheet.show((root.context as BaseActivity).supportFragmentManager)
49+
val activity = ContextHelper.unwrapActivity<BaseActivity>(root.context)
50+
channelOptionsSheet.show(activity.supportFragmentManager)
4951
true
5052
}
5153

app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt

Lines changed: 6 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,35 @@ import android.annotation.SuppressLint
44
import android.content.res.Configuration
55
import android.os.Bundle
66
import android.view.View
7-
import android.view.ViewGroup.MarginLayoutParams
87
import androidx.core.os.bundleOf
98
import androidx.core.view.children
109
import androidx.core.view.isGone
1110
import androidx.core.view.isVisible
12-
import androidx.core.view.updateLayoutParams
1311
import androidx.fragment.app.activityViewModels
1412
import androidx.lifecycle.lifecycleScope
1513
import androidx.recyclerview.widget.GridLayoutManager
16-
import androidx.recyclerview.widget.LinearLayoutManager
1714
import androidx.recyclerview.widget.RecyclerView
1815
import com.github.libretube.R
1916
import com.github.libretube.api.obj.StreamItem
20-
import com.github.libretube.api.obj.Subscription
2117
import com.github.libretube.constants.IntentData
2218
import com.github.libretube.constants.PreferenceKeys
2319
import com.github.libretube.databinding.FragmentSubscriptionsBinding
2420
import com.github.libretube.db.DatabaseHelper
2521
import com.github.libretube.db.DatabaseHolder
26-
import com.github.libretube.extensions.dpToPx
27-
import com.github.libretube.extensions.formatShort
2822
import com.github.libretube.extensions.toID
2923
import com.github.libretube.helpers.NavBarHelper
3024
import com.github.libretube.helpers.NavigationHelper
3125
import com.github.libretube.helpers.PreferenceHelper
3226
import com.github.libretube.obj.SelectableOption
33-
import com.github.libretube.ui.adapters.LegacySubscriptionAdapter
34-
import com.github.libretube.ui.adapters.SubscriptionChannelAdapter
3527
import com.github.libretube.ui.adapters.VideoCardsAdapter
3628
import com.github.libretube.ui.base.DynamicLayoutManagerFragment
37-
import com.github.libretube.ui.extensions.addOnBottomReachedListener
3829
import com.github.libretube.ui.extensions.setupFragmentAnimation
39-
import com.github.libretube.ui.models.CommonPlayerViewModel
4030
import com.github.libretube.ui.models.EditChannelGroupsModel
4131
import com.github.libretube.ui.models.SubscriptionsViewModel
4232
import com.github.libretube.ui.sheets.ChannelGroupsSheet
4333
import com.github.libretube.ui.sheets.FilterSortBottomSheet
4434
import com.github.libretube.ui.sheets.FilterSortBottomSheet.Companion.FILTER_SORT_REQUEST_KEY
35+
import com.github.libretube.ui.sheets.SubscriptionsBottomSheet
4536
import com.github.libretube.util.PlayingQueue
4637
import com.google.android.material.chip.Chip
4738
import kotlinx.coroutines.Dispatchers
@@ -52,7 +43,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
5243
private val binding get() = _binding!!
5344

5445
private val viewModel: SubscriptionsViewModel by activityViewModels()
55-
private val playerModel: CommonPlayerViewModel by activityViewModels()
5646
private val channelGroupsModel: EditChannelGroupsModel by activityViewModels()
5747
private var selectedFilterGroup
5848
set(value) = PreferenceHelper.putInt(PreferenceKeys.SELECTED_CHANNEL_GROUP, value)
@@ -81,9 +71,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
8171
field = value
8272
}
8373

84-
private val legacySubscriptionsAdapter = LegacySubscriptionAdapter()
85-
private val channelsAdapter = SubscriptionChannelAdapter()
86-
8774
override fun setLayoutManagers(gridItems: Int) {
8875
_binding?.subFeed?.layoutManager = GridLayoutManager(context, gridItems)
8976
}
@@ -97,25 +84,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
9784

9885
binding.subFeed.adapter = feedAdapter
9986

100-
val legacySubscriptions = PreferenceHelper.getBoolean(
101-
PreferenceKeys.LEGACY_SUBSCRIPTIONS,
102-
false
103-
)
104-
105-
if (legacySubscriptions) {
106-
binding.subChannels.layoutManager = GridLayoutManager(
107-
context,
108-
PreferenceHelper.getString(
109-
PreferenceKeys.LEGACY_SUBSCRIPTIONS_COLUMNS,
110-
"3"
111-
).toInt()
112-
)
113-
binding.subChannels.adapter = legacySubscriptionsAdapter
114-
} else {
115-
binding.subChannels.layoutManager = LinearLayoutManager(context)
116-
binding.subChannels.adapter = channelsAdapter
117-
}
118-
11987
// Check if the AppBarLayout is fully expanded
12088
binding.subscriptionsAppBar.addOnOffsetChangedListener { _, verticalOffset ->
12189
isAppBarFullyExpanded = verticalOffset == 0
@@ -141,7 +109,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
141109
// position to zero
142110
var alreadyShowedFeedOnce = false
143111
viewModel.videoFeed.observe(viewLifecycleOwner) { feed ->
144-
if (!viewModel.isCurrentTabSubChannels && feed != null) {
112+
if (feed != null) {
145113
lifecycleScope.launch {
146114
showFeed(!alreadyShowedFeedOnce)
147115
}
@@ -153,15 +121,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
153121
}
154122
}
155123

156-
// restore the scroll position, same conditions as above
157-
var alreadyShowedSubscriptionsOnce = false
158-
viewModel.subscriptions.observe(viewLifecycleOwner) {
159-
if (viewModel.isCurrentTabSubChannels && it != null) {
160-
showSubscriptions(!alreadyShowedSubscriptionsOnce)
161-
alreadyShowedSubscriptionsOnce = true
162-
}
163-
}
164-
165124
viewModel.feedProgress.observe(viewLifecycleOwner) { progress ->
166125
if (progress == null || progress.currentProgress == progress.total) {
167126
binding.feedProgressContainer.isGone = true
@@ -181,40 +140,15 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
181140
binding.toggleSubs.isVisible = true
182141

183142
binding.toggleSubs.setOnClickListener {
184-
binding.subProgress.isVisible = true
185-
binding.subRefresh.isRefreshing = true
186-
viewModel.isCurrentTabSubChannels = !viewModel.isCurrentTabSubChannels
187-
188-
lifecycleScope.launch {
189-
if (viewModel.isCurrentTabSubChannels) showSubscriptions() else showFeed()
190-
}
191-
192-
binding.subChannels.isVisible = viewModel.isCurrentTabSubChannels
193-
binding.subFeed.isGone = viewModel.isCurrentTabSubChannels
194-
}
195-
196-
binding.subChannels.addOnBottomReachedListener {
197-
val binding = _binding ?: return@addOnBottomReachedListener
198-
199-
if (viewModel.subscriptions.value != null && viewModel.isCurrentTabSubChannels) {
200-
binding.subRefresh.isRefreshing = true
201-
binding.subRefresh.isRefreshing = false
202-
}
203-
}
204-
205-
// add some extra margin to the subscribed channels while the mini player is visible
206-
// otherwise the last channel would be invisible
207-
playerModel.isMiniPlayerVisible.observe(viewLifecycleOwner) {
208-
binding.subChannels.updateLayoutParams<MarginLayoutParams> {
209-
bottomMargin = (if (it) 64f else 0f).dpToPx()
210-
}
143+
SubscriptionsBottomSheet()
144+
.show(childFragmentManager)
211145
}
212146

213147
binding.channelGroups.setOnCheckedStateChangeListener { group, _ ->
214148
selectedFilterGroup = group.children.indexOfFirst { it.id == group.checkedChipId }
215149

216150
lifecycleScope.launch {
217-
if (viewModel.isCurrentTabSubChannels) showSubscriptions() else showFeed()
151+
showFeed()
218152
}
219153
}
220154

@@ -226,17 +160,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
226160
ChannelGroupsSheet().show(childFragmentManager, null)
227161
}
228162

229-
// manually restore the recyclerview state due to https://github.com/material-components/material-components-android/issues/3473
230-
binding.subChannels.addOnScrollListener(object : RecyclerView.OnScrollListener() {
231-
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
232-
super.onScrollStateChanged(recyclerView, newState)
233-
viewModel.subChannelsRecyclerViewState =
234-
binding.subChannels.layoutManager?.onSaveInstanceState()?.takeIf {
235-
binding.subChannels.computeVerticalScrollOffset() != 0
236-
}
237-
}
238-
})
239-
240163
binding.subFeed.addOnScrollListener(object : RecyclerView.OnScrollListener() {
241164
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
242165
super.onScrollStateChanged(recyclerView, newState)
@@ -353,14 +276,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
353276
}
354277
}
355278

356-
@JvmName("filterSubsByGroup")
357-
private fun List<Subscription>.filterByGroup(groupIndex: Int): List<Subscription> {
358-
if (groupIndex == 0) return this
359-
360-
val group = channelGroupsModel.groups.value?.getOrNull(groupIndex - 1)
361-
return filter { group?.channels?.contains(it.url.toID()) != false }
362-
}
363-
364279
private fun List<StreamItem>.sortedBySelectedOrder() = when (selectedSortOrder) {
365280
0 -> this
366281
1 -> this.reversed()
@@ -399,7 +314,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
399314
}
400315
}
401316

402-
binding.subChannels.isGone = true
317+
403318
binding.subProgress.isGone = true
404319

405320
val notLoaded = viewModel.videoFeed.value.isNullOrEmpty()
@@ -421,42 +336,9 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
421336
}
422337
}
423338

424-
@SuppressLint("SetTextI18n")
425-
private fun showSubscriptions(restoreScrollState: Boolean = true) {
426-
val subscriptions =
427-
viewModel.subscriptions.value?.filterByGroup(selectedFilterGroup) ?: return
428-
429-
val legacySubscriptions = PreferenceHelper.getBoolean(
430-
PreferenceKeys.LEGACY_SUBSCRIPTIONS,
431-
false
432-
)
433-
434-
val adapter = if (legacySubscriptions) legacySubscriptionsAdapter else channelsAdapter
435-
adapter.submitList(subscriptions) {
436-
if (restoreScrollState) {
437-
binding.subFeed.layoutManager?.onRestoreInstanceState(viewModel.subChannelsRecyclerViewState)
438-
binding.subscriptionsAppBar.setExpanded(viewModel.subChannelsRecyclerViewState == null)
439-
} else {
440-
binding.subFeed.scrollToPosition(0)
441-
}
442-
}
443-
444-
binding.subRefresh.isRefreshing = false
445-
binding.subProgress.isGone = true
446-
binding.subFeed.isGone = true
447-
448-
val notLoaded = viewModel.subscriptions.value.isNullOrEmpty()
449-
binding.subChannels.isGone = notLoaded
450-
binding.emptyFeed.isVisible = notLoaded
451-
452-
val subCount = subscriptions.size.toLong().formatShort()
453-
binding.toggleSubs.text = "${getString(R.string.subscriptions)} ($subCount)"
454-
}
455-
456339
override fun onConfigurationChanged(newConfig: Configuration) {
457340
super.onConfigurationChanged(newConfig)
458341
// manually restore the recyclerview state after rotation due to https://github.com/material-components/material-components-android/issues/3473
459-
binding.subChannels.layoutManager?.onRestoreInstanceState(viewModel.subChannelsRecyclerViewState)
460342
binding.subFeed.layoutManager?.onRestoreInstanceState(viewModel.subFeedRecyclerViewState)
461343
}
462344

app/src/main/java/com/github/libretube/ui/models/SubscriptionsViewModel.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ class SubscriptionsViewModel : ViewModel() {
2424
var subscriptions = MutableLiveData<List<Subscription>?>()
2525
val feedProgress = MutableLiveData<FeedProgress?>()
2626

27-
var isCurrentTabSubChannels = false
28-
var subChannelsRecyclerViewState: Parcelable? = null
2927
var subFeedRecyclerViewState: Parcelable? = null
3028

3129
fun fetchFeed(context: Context, forceRefresh: Boolean) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.github.libretube.ui.sheets
2+
3+
import android.os.Bundle
4+
import android.view.View
5+
import androidx.core.widget.addTextChangedListener
6+
import androidx.fragment.app.activityViewModels
7+
import com.github.libretube.R
8+
import com.github.libretube.databinding.SheetSubscriptionsBinding
9+
import com.github.libretube.ui.adapters.SubscriptionChannelAdapter
10+
import com.github.libretube.ui.models.SubscriptionsViewModel
11+
12+
class SubscriptionsBottomSheet : ExpandedBottomSheet(R.layout.sheet_subscriptions) {
13+
private var _binding: SheetSubscriptionsBinding? = null
14+
private val binding get() = _binding!!
15+
private val adapter = SubscriptionChannelAdapter()
16+
17+
private val viewModel: SubscriptionsViewModel by activityViewModels()
18+
19+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
20+
_binding = SheetSubscriptionsBinding.bind(view)
21+
super.onViewCreated(view, savedInstanceState)
22+
23+
binding.channelsRecycler.adapter = adapter
24+
25+
binding.subscriptionsSearchInput.addTextChangedListener { query ->
26+
showFilteredSubscriptions(adapter, query.toString())
27+
}
28+
29+
viewModel.subscriptions.observe(viewLifecycleOwner) {
30+
showFilteredSubscriptions(adapter, binding.subscriptionsSearchInput.text.toString())
31+
}
32+
}
33+
34+
private fun showFilteredSubscriptions(adapter: SubscriptionChannelAdapter, query: String) {
35+
val loweredQuery = query.lowercase()
36+
37+
val filteredSubscriptions = viewModel.subscriptions.value.orEmpty()
38+
.filter { it.name.lowercase().contains(loweredQuery) }
39+
adapter.submitList(filteredSubscriptions)
40+
}
41+
}

app/src/main/res/layout/channel_subscription_row.xml

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,30 @@
2828
android:textSize="16sp"
2929
tools:text="Channel Name" />
3030

31-
<com.google.android.material.button.MaterialButton
32-
android:id="@+id/notification_bell"
33-
style="@style/ElevatedIconButton"
34-
android:layout_gravity="center"
35-
android:tooltipText="@string/notifications"
36-
app:icon="@drawable/ic_notification"
37-
tools:targetApi="m" />
38-
39-
<com.google.android.material.button.MaterialButton
40-
android:id="@+id/subscription_subscribe"
41-
style="@style/Widget.Material3.Button.ElevatedButton"
31+
<com.google.android.material.button.MaterialButtonGroup
4232
android:layout_width="wrap_content"
4333
android:layout_height="wrap_content"
44-
android:layout_gravity="center"
4534
android:layout_marginEnd="8dp"
46-
android:stateListAnimator="@null"
47-
android:text="@string/unsubscribe"
48-
android:textColor="?android:attr/textColorPrimary"
49-
android:textSize="12sp"
50-
app:cornerRadius="20dp"
51-
app:elevation="10dp" />
35+
android:paddingTop="0dp"
36+
android:paddingBottom="0dp"
37+
style="@style/Widget.Material3.MaterialSplitButton">
38+
39+
<com.google.android.material.button.MaterialButton
40+
style="?materialIconButtonFilledTonalStyle"
41+
android:id="@+id/notification_bell"
42+
android:layout_width="wrap_content"
43+
android:layout_height="wrap_content"
44+
android:layout_gravity="center"
45+
android:tooltipText="@string/notifications"
46+
app:icon="@drawable/ic_notification" />
47+
48+
<com.google.android.material.button.MaterialButton
49+
android:id="@+id/subscription_subscribe"
50+
style="?materialButtonTonalStyle"
51+
android:layout_width="wrap_content"
52+
android:layout_height="wrap_content"
53+
android:text="@string/unsubscribe"
54+
android:textSize="12sp"/>
55+
56+
</com.google.android.material.button.MaterialButtonGroup>
5257
</LinearLayout>

app/src/main/res/layout/fragment_subscriptions.xml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,6 @@
195195
android:layout_width="match_parent"
196196
android:layout_height="match_parent"
197197
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
198-
199-
<androidx.recyclerview.widget.RecyclerView
200-
android:id="@+id/sub_channels"
201-
android:layout_width="match_parent"
202-
android:layout_height="match_parent"
203-
android:visibility="gone"
204-
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
205198
</androidx.coordinatorlayout.widget.CoordinatorLayout>
206199
</com.github.libretube.ui.views.CustomSwipeToRefresh>
207200
</RelativeLayout>

0 commit comments

Comments
 (0)