feat: split downloads fragment into audio and video category

This commit is contained in:
Bnyro 2024-10-06 15:03:53 +02:00
parent db0dc4c4fc
commit f0fb359b5d
5 changed files with 256 additions and 137 deletions

View File

@ -7,7 +7,6 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import com.github.libretube.constants.IntentData
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.enums.FileType
import com.github.libretube.extensions.toAndroidUri

View File

@ -16,9 +16,11 @@ import com.github.libretube.databinding.DownloadedMediaRowBinding
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.DownloadWithItems
import com.github.libretube.extensions.formatAsFileSize
import com.github.libretube.helpers.BackgroundHelper
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.ui.activities.OfflinePlayerActivity
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.fragments.DownloadTab
import com.github.libretube.ui.sheets.DownloadOptionsBottomSheet
import com.github.libretube.ui.sheets.DownloadOptionsBottomSheet.Companion.DELETE_DOWNLOAD_REQUEST_KEY
import com.github.libretube.ui.viewholders.DownloadsViewHolder
@ -32,6 +34,7 @@ import kotlin.io.path.fileSize
class DownloadsAdapter(
private val context: Context,
private val downloadTab: DownloadTab,
private val downloads: MutableList<DownloadWithItems>,
private val toggleDownload: (DownloadWithItems) -> Boolean
) : RecyclerView.Adapter<DownloadsViewHolder>() {
@ -98,9 +101,13 @@ class DownloadsAdapter(
}
root.setOnClickListener {
val intent = Intent(root.context, OfflinePlayerActivity::class.java)
intent.putExtra(IntentData.videoId, download.videoId)
root.context.startActivity(intent)
if (downloadTab == DownloadTab.VIDEO) {
val intent = Intent(root.context, OfflinePlayerActivity::class.java)
intent.putExtra(IntentData.videoId, download.videoId)
root.context.startActivity(intent)
} else {
BackgroundHelper.playOnBackgroundOffline(root.context, download.videoId)
}
}
root.setOnLongClickListener {

View File

@ -11,20 +11,26 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.github.libretube.R
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentDownloadContentBinding
import com.github.libretube.databinding.FragmentDownloadsBinding
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.DownloadWithItems
import com.github.libretube.enums.FileType
import com.github.libretube.extensions.ceilHalf
import com.github.libretube.extensions.formatAsFileSize
import com.github.libretube.helpers.BackgroundHelper
import com.github.libretube.extensions.serializable
import com.github.libretube.helpers.DownloadHelper
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.obj.DownloadStatus
@ -35,20 +41,77 @@ import com.github.libretube.ui.base.DynamicLayoutManagerFragment
import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.viewholders.DownloadsViewHolder
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.io.path.fileSize
class DownloadsFragment : DynamicLayoutManagerFragment() {
enum class DownloadTab {
VIDEO,
AUDIO
}
class DownloadsFragment : Fragment() {
private var _binding: FragmentDownloadsBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDownloadsBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.downloadsPager.adapter = DownloadsFragmentAdapter(this)
TabLayoutMediator(binding.tabLayout, binding.downloadsPager) { tab, position ->
tab.text = when (position) {
DownloadTab.VIDEO.ordinal -> getString(R.string.video)
DownloadTab.AUDIO.ordinal -> getString(R.string.audio)
else -> throw IllegalArgumentException()
}
}.attach()
}
fun bindDownloadService() {
childFragmentManager.fragments.filterIsInstance<DownloadsFragmentPage>().forEach {
it.bindDownloadService()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
class DownloadsFragmentAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
override fun getItemCount() = DownloadTab.entries.size
override fun createFragment(position: Int): Fragment {
return DownloadsFragmentPage().apply {
arguments = bundleOf(IntentData.currentPosition to DownloadTab.entries[position])
}
}
}
class DownloadsFragmentPage : DynamicLayoutManagerFragment() {
private lateinit var adapter: DownloadsAdapter
private var _binding: FragmentDownloadContentBinding? = null
private val binding get() = _binding!!
private var binder: DownloadService.LocalBinder? = null
private val downloads = mutableListOf<DownloadWithItems>()
private val downloadReceiver = DownloadReceiver()
private lateinit var downloadTabSelf: DownloadTab
private val serviceConnection = object : ServiceConnection {
var isBound = false
@ -71,21 +134,28 @@ class DownloadsFragment : DynamicLayoutManagerFragment() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.downloadTabSelf = requireArguments().serializable(IntentData.currentPosition)!!
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDownloadsBinding.inflate(inflater)
_binding = FragmentDownloadContentBinding.inflate(layoutInflater)
return binding.root
}
override fun setLayoutManagers(gridItems: Int) {
_binding?.downloads?.layoutManager = GridLayoutManager(context, gridItems.ceilHalf())
_binding?.downloadsRecView?.layoutManager = GridLayoutManager(context, gridItems.ceilHalf())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var selectedSortType =
PreferenceHelper.getInt(PreferenceKeys.SELECTED_DOWNLOAD_SORT_TYPE, 0)
val filterOptions = resources.getStringArray(R.array.downloadSortOptions)
@ -95,6 +165,7 @@ class DownloadsFragment : DynamicLayoutManagerFragment() {
binding.sortType.text = filterOptions[index]
if (::adapter.isInitialized) {
sortDownloadList(index, selectedSortType)
adapter.notifyDataSetChanged()
}
selectedSortType = index
PreferenceHelper.putInt(
@ -104,96 +175,111 @@ class DownloadsFragment : DynamicLayoutManagerFragment() {
}.show(childFragmentManager)
}
val dbDownloads = runBlocking(Dispatchers.IO) {
Database.downloadDao().getAll()
}.takeIf { it.isNotEmpty() } ?: return
downloads.clear()
downloads.addAll(dbDownloads)
binding.downloadsEmpty.isGone = true
binding.downloads.isVisible = true
adapter = DownloadsAdapter(requireContext(), downloads) {
var isDownloading = false
val ids = it.downloadItems
.filter { item -> item.path.fileSize() < item.downloadSize }
.map { item -> item.id }
if (!serviceConnection.isBound) {
DownloadHelper.startDownloadService(requireContext())
bindDownloadService(ids.toIntArray())
return@DownloadsAdapter true
lifecycleScope.launch {
val dbDownloads = withContext(Dispatchers.IO) {
Database.downloadDao().getAll()
}
binder?.getService()?.let { service ->
isDownloading = ids.any { id -> service.isDownloading(id) }
downloads.clear()
downloads.addAll(dbDownloads.filter { dl ->
when (downloadTabSelf) {
DownloadTab.AUDIO -> {
dl.downloadItems.any { it.type == FileType.AUDIO } && dl.downloadItems.none { it.type == FileType.VIDEO }
}
ids.forEach { id ->
if (isDownloading) {
service.pause(id)
} else {
service.resume(id)
DownloadTab.VIDEO -> {
dl.downloadItems.any { it.type == FileType.VIDEO }
}
}
}
return@DownloadsAdapter isDownloading.not()
}
sortDownloadList(selectedSortType)
binding.downloads.adapter = adapter
})
val itemTouchCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int = makeMovementFlags(0, ItemTouchHelper.LEFT)
if (downloads.isEmpty()) return@launch
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
sortDownloadList(selectedSortType)
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
adapter.showDeleteDialog(requireContext(), viewHolder.absoluteAdapterPosition)
// put the item back to the center, as it's currently out of the screen
adapter.restoreItem(viewHolder.absoluteAdapterPosition)
}
}
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.downloads)
adapter = DownloadsAdapter(requireContext(), downloadTabSelf, downloads) {
var isDownloading = false
val ids = it.downloadItems
.filter { item -> item.path.fileSize() < item.downloadSize }
.map { item -> item.id }
binding.downloads.adapter?.registerAdapterDataObserver(
object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
toggleButtonsVisibility()
if (!serviceConnection.isBound) {
DownloadHelper.startDownloadService(requireContext())
bindDownloadService(ids.toIntArray())
return@DownloadsAdapter true
}
}
)
toggleButtonsVisibility()
binder?.getService()?.let { service ->
isDownloading = ids.any { id -> service.isDownloading(id) }
ids.forEach { id ->
if (isDownloading) {
service.pause(id)
} else {
service.resume(id)
}
}
}
return@DownloadsAdapter isDownloading.not()
}
binding.downloadsRecView.adapter = adapter
val itemTouchCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int = makeMovementFlags(0, ItemTouchHelper.LEFT)
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
adapter.showDeleteDialog(
requireContext(),
viewHolder.absoluteAdapterPosition
)
// put the item back to the center, as it's currently out of the screen
adapter.restoreItem(viewHolder.absoluteAdapterPosition)
}
}
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.downloadsRecView)
binding.downloadsRecView.adapter?.registerAdapterDataObserver(
object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
toggleVisibilities()
}
}
)
toggleVisibilities()
}
binding.deleteAll.setOnClickListener {
showDeleteAllDialog(binding.root.context, adapter)
}
}
private fun toggleButtonsVisibility() {
private fun toggleVisibilities() {
val binding = _binding ?: return
val isEmpty = binding.downloads.adapter?.itemCount == 0
val isEmpty = downloads.isEmpty()
binding.downloadsEmpty.isVisible = isEmpty
binding.downloads.isGone = isEmpty
binding.sortType.isGone = isEmpty
binding.downloadsContainer.isGone = isEmpty
binding.deleteAll.isGone = isEmpty
}
private fun sortDownloadList(sortType: Int, previousSortType: Int? = null) {
if (previousSortType == null && sortType == 1) {
downloads.reverse()
adapter.notifyDataSetChanged()
}
if (previousSortType != null && sortType != previousSortType) {
downloads.reverse()
adapter.notifyDataSetChanged()
}
}
@ -246,7 +332,7 @@ class DownloadsFragment : DynamicLayoutManagerFragment() {
it.downloadItems.any { item -> item.id == id }
}
val view =
_binding?.downloads?.findViewHolderForAdapterPosition(index) as? DownloadsViewHolder
_binding?.downloadsRecView?.findViewHolderForAdapterPosition(index) as? DownloadsViewHolder
view?.binding?.apply {
when (status) {
@ -293,4 +379,4 @@ class DownloadsFragment : DynamicLayoutManagerFragment() {
super.onDestroyView()
_binding = null
}
}
}

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent">
<LinearLayout
android:id="@+id/downloads_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/sort_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="6dp"
android:layout_marginEnd="4dp"
android:layout_marginVertical="4dp"
android:clickable="true"
android:drawablePadding="10dp"
android:focusable="true"
android:layout_gravity="end"
android:paddingVertical="5dp"
android:text="@string/sort_by"
android:textSize="15sp"
app:drawableEndCompat="@drawable/ic_sort"
android:background="@drawable/rounded_ripple" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloads_recView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<LinearLayout
android:id="@+id/downloads_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_download" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:gravity="center"
android:text="@string/emptyList"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/delete_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="18dp"
android:layout_marginBottom="18dp"
android:contentDescription="@string/delete_all"
android:src="@drawable/ic_delete"
android:tooltipText="@string/delete"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:targetApi="o"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,79 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
android:background="?android:attr/colorBackground">
<TextView
android:id="@+id/sort_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:clickable="true"
android:drawablePadding="10dp"
android:focusable="true"
android:fontFamily="@font/roboto"
android:paddingVertical="5dp"
android:text="@string/sort_by"
android:textSize="15sp"
android:textStyle="bold"
android:visibility="gone"
app:drawableEndCompat="@drawable/ic_sort"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/downloads_empty"
android:id="@+id/downloads_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_download" />
<TextView
android:layout_width="wrap_content"
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:gravity="center"
android:text="@string/emptyList"
android:textSize="20sp"
android:textStyle="bold" />
app:tabMode="scrollable" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/downloads_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloads"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sort_type" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/delete_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="18dp"
android:layout_marginBottom="18dp"
android:contentDescription="@string/shuffle"
android:src="@drawable/ic_delete"
android:tooltipText="@string/delete"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:targetApi="o"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>