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.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.DatabaseHolder.Database import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.enums.FileType import com.github.libretube.enums.FileType
import com.github.libretube.extensions.toAndroidUri 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.DatabaseHolder
import com.github.libretube.db.obj.DownloadWithItems import com.github.libretube.db.obj.DownloadWithItems
import com.github.libretube.extensions.formatAsFileSize import com.github.libretube.extensions.formatAsFileSize
import com.github.libretube.helpers.BackgroundHelper
import com.github.libretube.helpers.ImageHelper import com.github.libretube.helpers.ImageHelper
import com.github.libretube.ui.activities.OfflinePlayerActivity import com.github.libretube.ui.activities.OfflinePlayerActivity
import com.github.libretube.ui.base.BaseActivity 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
import com.github.libretube.ui.sheets.DownloadOptionsBottomSheet.Companion.DELETE_DOWNLOAD_REQUEST_KEY import com.github.libretube.ui.sheets.DownloadOptionsBottomSheet.Companion.DELETE_DOWNLOAD_REQUEST_KEY
import com.github.libretube.ui.viewholders.DownloadsViewHolder import com.github.libretube.ui.viewholders.DownloadsViewHolder
@ -32,6 +34,7 @@ import kotlin.io.path.fileSize
class DownloadsAdapter( class DownloadsAdapter(
private val context: Context, private val context: Context,
private val downloadTab: DownloadTab,
private val downloads: MutableList<DownloadWithItems>, private val downloads: MutableList<DownloadWithItems>,
private val toggleDownload: (DownloadWithItems) -> Boolean private val toggleDownload: (DownloadWithItems) -> Boolean
) : RecyclerView.Adapter<DownloadsViewHolder>() { ) : RecyclerView.Adapter<DownloadsViewHolder>() {
@ -98,9 +101,13 @@ class DownloadsAdapter(
} }
root.setOnClickListener { root.setOnClickListener {
val intent = Intent(root.context, OfflinePlayerActivity::class.java) if (downloadTab == DownloadTab.VIDEO) {
intent.putExtra(IntentData.videoId, download.videoId) val intent = Intent(root.context, OfflinePlayerActivity::class.java)
root.context.startActivity(intent) intent.putExtra(IntentData.videoId, download.videoId)
root.context.startActivity(intent)
} else {
BackgroundHelper.playOnBackgroundOffline(root.context, download.videoId)
}
} }
root.setOnLongClickListener { root.setOnLongClickListener {

View File

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

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"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?android:attr/colorBackground"> 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 <LinearLayout
android:id="@+id/downloads_empty" android:id="@+id/downloads_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:gravity="center" android:orientation="vertical">
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView <com.google.android.material.tabs.TabLayout
android:layout_width="100dp" android:id="@+id/tab_layout"
android:layout_height="100dp" android:layout_width="match_parent"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_download" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp" app:tabMode="scrollable" />
android:gravity="center"
android:text="@string/emptyList" <androidx.viewpager2.widget.ViewPager2
android:textSize="20sp" android:id="@+id/downloads_pager"
android:textStyle="bold" /> android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout> </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> </androidx.constraintlayout.widget.ConstraintLayout>