From ece2cdf631e2b1f02cc03f2439c8ea290af0a421 Mon Sep 17 00:00:00 2001 From: deniscerri <64997243+deniscerri@users.noreply.github.com> Date: Sat, 31 May 2025 16:14:59 +0200 Subject: [PATCH] add function to modify selected items in multiple download card --- .../ytdl/database/dao/DownloadDao.kt | 23 +- .../database/repository/DownloadRepository.kt | 2 +- .../database/viewmodel/DownloadViewModel.kt | 111 +++++-- .../ConfigureMultipleDownloadsAdapter.kt | 103 ++++++- .../DownloadMultipleBottomSheetDialog.kt | 285 +++++++++++++----- .../java/com/deniscerri/ytdl/util/UiUtil.kt | 68 +++++ .../baseline_format_list_numbered_24.xml | 5 + app/src/main/res/drawable/ic_select_all.xml | 2 +- .../res/drawable/rounded_corner_index.xml | 14 + app/src/main/res/layout/download_card.xml | 36 ++- .../layout/download_multiple_bottom_sheet.xml | 125 ++++++-- app/src/main/res/layout/playlist_item.xml | 3 +- .../main/res/layout/select_playlist_items.xml | 2 + .../main/res/layout/select_range_dialog.xml | 54 ++++ .../main/res/menu/download_multiple_menu.xml | 8 +- .../select_multiple_items_menu_context.xml | 30 ++ app/src/main/res/values/strings.xml | 1 + 17 files changed, 746 insertions(+), 126 deletions(-) create mode 100644 app/src/main/res/drawable/baseline_format_list_numbered_24.xml create mode 100644 app/src/main/res/drawable/rounded_corner_index.xml create mode 100644 app/src/main/res/layout/select_range_dialog.xml create mode 100644 app/src/main/res/menu/select_multiple_items_menu_context.xml diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt index b2e85401..1babf04d 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt @@ -54,11 +54,16 @@ interface DownloadDao { """) fun getProcessingDownloadTypes() : List - @Query(""" - SELECT DISTINCT container from downloads where status = 'Processing' - """) + + @Query("SELECT DISTINCT type from downloads where status = 'Processing' and id in (:ids)") + fun getProcessingDownloadTypesByIDs(ids: List) : List + + @Query("""SELECT DISTINCT container from downloads where status = 'Processing'""") fun getProcessingDownloadContainers() : List + @Query("""SELECT DISTINCT container from downloads where id in (:ids)""") + fun getDownloadContainersByIDs(ids: List) : List + @Query("UPDATE downloads set status = 'Processing' WHERE id in (:ids)") suspend fun updateItemsToProcessing(ids: List) @@ -74,9 +79,15 @@ interface DownloadDao { @Query("UPDATE downloads set downloadPath=:path WHERE status ='Processing'") suspend fun updateProcessingDownloadPath(path: String) + @Query("UPDATE downloads set downloadPath=:path WHERE id in (:ids)") + suspend fun updateDownloadPathByIDs(ids: List, path: String) + @Query("UPDATE downloads set container=:cont WHERE status ='Processing'") suspend fun updateProcessingContainer(cont: String) + @Query("UPDATE downloads set container=:cont WHERE id in (:ids)") + suspend fun updateContainerByIds(ids: List, cont: String) + @Query("SELECT * FROM downloads WHERE status='Active'") fun getActiveDownloadsList() : List @@ -356,6 +367,12 @@ interface DownloadDao { @Query("UPDATE downloads set incognito=:incognito WHERE status='Processing'") suspend fun updateProcessingIncognito(incognito: Boolean) + @Query("UPDATE downloads set incognito=:incognito WHERE id in (:ids)") + suspend fun updateIncognitoByIDs(incognito: Boolean, ids: List) + @Query("SELECT COUNT(id) FROM downloads WHERE status='Processing' AND incognito='1'") fun getProcessingAsIncognitoCount(): Int + + @Query("SELECT COUNT(id) FROM downloads WHERE status='Processing' AND incognito='1' and id in (:ids)") + fun getProcessingAsIncognitoCountByIDs(ids: List): Int } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt index 5894c125..4d10de83 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt @@ -140,7 +140,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) { downloadDao.deleteProcessingByUrl(url) } - fun getProcessingDownloads() : List { + fun getAllProcessingDownloads() : List { return downloadDao.getProcessingDownloadsList() } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt index efddab32..1d2ea33b 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt @@ -890,7 +890,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } suspend fun queueProcessingDownloads() : QueueDownloadsResult { - val processingItems = repository.getProcessingDownloads() + val processingItems = repository.getAllProcessingDownloads() return queueDownloads(processingItems) } @@ -1087,13 +1087,22 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel return dbManager.downloadDao.getDownloadIDsNotPresentInList(items.ifEmpty { listOf(-1L) }, status.map { it.toString() }) } - suspend fun moveProcessingToSavedCategory(){ - dao.updateProcessingtoSavedStatus() + suspend fun moveProcessingToSavedCategory(selectedItems: List?){ + if (selectedItems.isNullOrEmpty()) { + dao.updateProcessingtoSavedStatus() + }else { + repository.setDownloadStatusMultiple(selectedItems, DownloadRepository.Status.Saved) + } } - fun updateAllProcessingFormats(formatTuples : List) = viewModelScope.launch(Dispatchers.IO) { - val items = repository.getProcessingDownloads() + fun updateAllProcessingFormats(selectedItems: List?, formatTuples : List) = viewModelScope.launch(Dispatchers.IO) { + val items = if (selectedItems.isNullOrEmpty()) { + repository.getAllProcessingDownloads() + }else { + repository.getAllItemsByIDs(selectedItems) + } + items.forEach { val ft = formatTuples.first { ft -> ft.url == it.url }.formatTuple ft.format?.apply { @@ -1112,28 +1121,48 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } - suspend fun updateProcessingCommandFormat(format: Format){ - val items = repository.getProcessingDownloads() + suspend fun updateProcessingCommandFormat(selectedItems: List?, format: Format){ + val items = if (selectedItems.isNullOrEmpty()) { + repository.getAllProcessingDownloads() + }else { + repository.getAllItemsByIDs(selectedItems) + } + items.forEach { it.format = format repository.update(it) } } - suspend fun updateProcessingContainer(cont: String) { + suspend fun updateProcessingContainer(checkedItems: List?, cont: String) { var container = "" if (cont != resources.getString(R.string.defaultValue)) { container = cont } - dao.updateProcessingContainer(container) + + if (checkedItems.isNullOrEmpty()) { + dao.updateProcessingContainer(container) + }else { + dao.updateContainerByIds(checkedItems, container) + } + } - suspend fun updateProcessingDownloadPath(path: String){ - dao.updateProcessingDownloadPath(path) + suspend fun updateProcessingDownloadPath(selectedItems: List?, path: String){ + if (selectedItems.isNullOrEmpty()) { + dao.updateProcessingDownloadPath(path) + }else { + dao.updateDownloadPathByIDs(selectedItems, path) + } } - fun getProcessingDownloads() : List { - return repository.getProcessingDownloads() + fun getProcessingDownloads(checkedItems: List?) : List { + return if (checkedItems.isNullOrEmpty()) { + repository.getAllProcessingDownloads() + }else { + repository.getAllItemsByIDs(checkedItems) + } + } fun updateDownloadItemFormats(id: Long, list: List) = viewModelScope.launch(Dispatchers.IO) { @@ -1174,9 +1203,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel resultRepository.deleteByUrl(url) } - suspend fun continueUpdatingFormatsOnBackground(){ - val ids = repository.getProcessingDownloads().map { it.id } - dao.updateProcessingtoSavedStatus() + suspend fun continueUpdatingFormatsOnBackground(selectedItems: List?){ + val ids = if (selectedItems.isNullOrEmpty()) { + repository.getAllProcessingDownloads().map { it.id } + }else { + selectedItems + } + + moveProcessingToSavedCategory(selectedItems) val id = System.currentTimeMillis().toInt() val workRequest = OneTimeWorkRequestBuilder() @@ -1213,8 +1247,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } - suspend fun updateProcessingType(newType: Type) { - val processing = repository.getProcessingDownloads() + suspend fun updateProcessingType(selectedItems: List?, newType: Type) { + val processing = if (selectedItems.isNullOrEmpty()) { + repository.getAllProcessingDownloads() + }else{ + repository.getAllItemsByIDs(selectedItems) + } + processing.apply { val new = switchDownloadType(this, newType) new.forEach { @@ -1224,7 +1263,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } suspend fun updateProcessingDownloadTimeAndQueueScheduled(time: Long) : QueueDownloadsResult { - val processing = repository.getProcessingDownloads() + val processing = repository.getAllProcessingDownloads() processing.forEach { it.downloadStartTime = time it.status = DownloadRepository.Status.Scheduled.toString() @@ -1232,8 +1271,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel return queueDownloads(processing) } - fun checkIfAllProcessingItemsHaveSameType() : Pair { - val types = dao.getProcessingDownloadTypes() + fun checkIfAllProcessingItemsHaveSameType(selectedItems: List?) : Pair { + val types = if (!selectedItems.isNullOrEmpty()) { + dao.getProcessingDownloadTypesByIDs(selectedItems) + }else { + dao.getProcessingDownloadTypes() + } + if (types.isEmpty()) { return Pair(false, Type.command) } @@ -1241,8 +1285,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel return Pair(types.size == 1, Type.valueOf(types.first())) } - fun checkIfAllProcessingItemsHaveSameContainer() : Pair { - val containers = dao.getProcessingDownloadContainers() + fun checkIfAllProcessingItemsHaveSameContainer(checkedItems: List?) : Pair { + val containers = if (checkedItems.isNullOrEmpty()) { + dao.getProcessingDownloadContainers() + }else { + dao.getDownloadContainersByIDs(checkedItems) + } + return Pair(containers.size == 1, containers.first()) } @@ -1277,12 +1326,20 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel return dao.getScheduledIDsBetweenTwoItems(item1, item2) } - suspend fun updateProcessingIncognito(incognito: Boolean) { - dao.updateProcessingIncognito(incognito) + suspend fun updateProcessingIncognito(selectedItems: List?, incognito: Boolean) { + if (selectedItems.isNullOrEmpty()) { + dao.updateProcessingIncognito(incognito) + }else { + dao.updateIncognitoByIDs(incognito, selectedItems) + } } - fun areAllProcessingIncognito() : Boolean { - return dao.getProcessingAsIncognitoCount() > 0 + fun areAllProcessingIncognito(selectedItems: List?) : Boolean { + return if (selectedItems.isNullOrEmpty()) { + dao.getProcessingAsIncognitoCount() > 0 + }else { + dao.getProcessingAsIncognitoCountByIDs(selectedItems) > 0 + } } fun cancelDownloadOnly(id : Long) { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ConfigureMultipleDownloadsAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ConfigureMultipleDownloadsAdapter.kt index 282fdc11..e2a0053a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ConfigureMultipleDownloadsAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ConfigureMultipleDownloadsAdapter.kt @@ -1,5 +1,6 @@ package com.deniscerri.ytdl.ui.adapter +import android.annotation.SuppressLint import android.app.Activity import android.content.SharedPreferences import android.os.Handler @@ -7,6 +8,7 @@ import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.CheckBox import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView @@ -32,6 +34,10 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener private val activity: Activity private val sharedPreferences : SharedPreferences + private var checkedItems: MutableSet = mutableSetOf() + private var currentItems: Set = setOf() + private var _isCheckingItems: Boolean = false + private var inverted: Boolean = false init { this.onItemClickListener = onItemClickListener @@ -53,6 +59,69 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener return ViewHolder(cardView) } + fun isCheckingItems() : Boolean { + return _isCheckingItems + } + + fun getCheckedItemsOrNull(): List? { + val res = if (inverted) { + currentItems.filter { !checkedItems.contains(it) } + }else { + checkedItems + } + + return res.toList().ifEmpty { null } + } + + fun getCheckedItemsSize() : Int { + return if (inverted){ + currentItems.size - checkedItems.size + }else{ + checkedItems.size + } + } + + fun removeItemsFromCheckList(ids: List) { + checkedItems.removeAll(ids.toSet()) + } + + @SuppressLint("NotifyDataSetChanged") + fun clearCheckedItems() { + _isCheckingItems = false + inverted = false + checkedItems = mutableSetOf() + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun selectItems(ids: List) { + checkedItems = mutableSetOf() + inverted = false + checkedItems.addAll(ids) + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun checkAll() { + checkedItems = mutableSetOf() + inverted = true + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun invertSelected() { + inverted = !inverted + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun initCheckingItems(itemIDs: List) { + currentItems = itemIDs.toSet() + _isCheckingItems = true + checkedItems = mutableSetOf() + notifyDataSetChanged() + } + override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = getItem(position) val card = holder.cardView @@ -162,18 +231,48 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener } } + val checkbox = card.findViewById(R.id.checkBox) + checkbox.isVisible = _isCheckingItems + checkbox.isChecked = (checkedItems.contains(item.id) && !inverted) || (inverted && !checkedItems.contains(item.id)) + checkbox.setOnClickListener { + if (checkbox.isChecked) { + if (inverted) checkedItems.remove(item.id) + else checkedItems.add(item.id) + onItemClickListener.onCardChecked(item.id) + }else { + if (inverted) checkedItems.add(item.id) + else checkedItems.remove(item.id) + onItemClickListener.onCardUnChecked(item.id) + } + } + + val index = card.findViewById(R.id.index) + index.isVisible = _isCheckingItems + index.text = (position + 1).toString() + card.setOnClickListener { - onItemClickListener.onCardClick(item.id) + if (_isCheckingItems) { + checkbox.performClick() + }else { + onItemClickListener.onCardClick(item.id) + } } card.setOnLongClickListener { - onItemClickListener.onDelete(item.id); true + if (_isCheckingItems) { + checkbox.performClick() + }else{ + onItemClickListener.onDelete(item.id) + } + true } } interface OnItemClickListener { fun onButtonClick(id: Long) fun onCardClick(id: Long) + fun onCardChecked(id: Long) + fun onCardUnChecked(id: Long) fun onDelete(id: Long) } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt index d974c648..c8c80a51 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt @@ -9,6 +9,7 @@ import android.content.SharedPreferences import android.content.res.Configuration import android.graphics.Canvas import android.graphics.Color +import android.os.Build import android.os.Bundle import android.util.DisplayMetrics import android.view.LayoutInflater @@ -16,14 +17,18 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.Window +import android.widget.CheckBox +import android.widget.LinearLayout import android.widget.PopupMenu import android.widget.TextView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.os.bundleOf import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.children +import androidx.core.view.get import androidx.core.view.isVisible import androidx.core.view.setPadding import androidx.core.view.updatePadding @@ -59,6 +64,7 @@ import com.google.android.material.button.MaterialButton import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.elevation.SurfaceColors +import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import kotlinx.coroutines.CancellationException @@ -68,6 +74,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.math.abs class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), ConfigureMultipleDownloadsAdapter.OnItemClickListener, View.OnClickListener, ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener { @@ -81,6 +88,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure private lateinit var behavior: BottomSheetBehavior private lateinit var bottomAppBar: BottomAppBar private lateinit var filesize : TextView + private var itemsFileSize: Long = 0L private lateinit var count : TextView private lateinit var downloadBtn : MaterialButton private lateinit var scheduleBtn : MaterialButton @@ -98,6 +106,10 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure private lateinit var containerBtn : MenuItem private lateinit var containerTextView: TextView + private lateinit var multipleSelectHeader: ConstraintLayout + private lateinit var selectItemsMenuBtn: MaterialButton + private lateinit var selectRangeBtn: MaterialButton + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] @@ -182,28 +194,51 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure lifecycleScope.launch { downloadViewModel.processingDownloads.collectLatest { items -> processingItemsCount = items.size + currentDownloadIDs = items.map { it.id } count.text = "$processingItemsCount ${getString(R.string.selected)}" listAdapter.submitList(items) updateFileSize(items) if (items.isNotEmpty()){ - if (items.all { it2 -> it2.type == items[0].type }) { - formatBtn.icon?.alpha = 255 - if (items[0].type != DownloadViewModel.Type.command) { - moreBtn.icon?.alpha = 255 + lifecycleScope.launch { + val haveSameType = if (listAdapter.isCheckingItems()) { + val res = withContext(Dispatchers.IO) { + downloadViewModel.checkIfAllProcessingItemsHaveSameType(listAdapter.getCheckedItemsOrNull()) + } + + res.first + }else { + items.all { it2 -> it2.type == items[0].type } } - containerBtn.isVisible = items.first().type != DownloadViewModel.Type.command - } else { - formatBtn.icon?.alpha = 30 - moreBtn.icon?.alpha = 30 - } + if (haveSameType) { + formatBtn.icon?.alpha = 255 + if (items[0].type != DownloadViewModel.Type.command) { + moreBtn.icon?.alpha = 255 + } - if (items.all { it.container == items[0].container }) { - setContainerText(items[0].container) - }else{ - setContainerText("") + containerBtn.isVisible = items.first().type != DownloadViewModel.Type.command + }else { + formatBtn.icon?.alpha = 30 + moreBtn.icon?.alpha = 30 + } + + val haveSameContainer = if (listAdapter.isCheckingItems()) { + val res = withContext(Dispatchers.IO) { + downloadViewModel.checkIfAllProcessingItemsHaveSameContainer(listAdapter.getCheckedItemsOrNull()) + } + + res.first + }else { + items.all { it.container == items[0].container } + } + + if (haveSameContainer) { + setContainerText(items[0].container) + }else { + setContainerText("") + } } val type = items.first().type @@ -233,7 +268,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure toggleLoading(true) lifecycleScope.launch { withContext(Dispatchers.IO){ - downloadViewModel.deleteAllWithID(currentDownloadIDs) historyViewModel.deleteAllWithIDsCheckFiles(currentHistoryIDs) val result = downloadViewModel.updateProcessingDownloadTimeAndQueueScheduled(cal.timeInMillis) if (result.message.isNotBlank()){ @@ -257,7 +291,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure toggleLoading(true) lifecycleScope.launch { withContext(Dispatchers.IO){ - downloadViewModel.deleteAllWithID(currentDownloadIDs) historyViewModel.deleteAllWithIDsCheckFiles(currentHistoryIDs) val result = downloadViewModel.queueProcessingDownloads() if (result.message.isNotBlank()){ @@ -283,8 +316,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure dd.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> lifecycleScope.launch{ withContext(Dispatchers.IO){ - downloadViewModel.deleteAllWithID(currentDownloadIDs) - downloadViewModel.moveProcessingToSavedCategory() + downloadViewModel.moveProcessingToSavedCategory(null) historyViewModel.deleteAllWithIDsCheckFiles(currentHistoryIDs) } @@ -300,7 +332,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure val formatListener = object : OnMultipleFormatClickListener { override fun onFormatClick(formatTuple: List) { - downloadViewModel.updateAllProcessingFormats(formatTuple) + downloadViewModel.updateAllProcessingFormats(listAdapter.getCheckedItemsOrNull(), formatTuple) } override fun onFormatUpdated(url: String, formats: List) { @@ -314,7 +346,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure override fun onContinueOnBackground() { requireActivity().lifecycleScope.launch { withContext(Dispatchers.IO){ - downloadViewModel.continueUpdatingFormatsOnBackground() + downloadViewModel.continueUpdatingFormatsOnBackground(listAdapter.getCheckedItemsOrNull()) } downloadViewModel.processingItemsJob?.cancel(CancellationException()) downloadViewModel.processingItemsJob = null @@ -326,7 +358,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure lifecycleScope.launch { val allIncognito = withContext(Dispatchers.IO){ - downloadViewModel.areAllProcessingIncognito() + downloadViewModel.areAllProcessingIncognito(listAdapter.getCheckedItemsOrNull()) } incognitoBtn.icon!!.apply { @@ -359,40 +391,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure bottomAppBar.setOnMenuItemClickListener { m: MenuItem -> when (m.itemId) { - R.id.incognito -> { - lifecycleScope.launch { - if (m.icon!!.alpha == 255) { - incognitoBtn.isEnabled = false - withContext(Dispatchers.IO) { - downloadViewModel.updateProcessingIncognito(false) - withContext(Dispatchers.Main){ - m.icon!!.alpha = 30 - m.isEnabled = true - } - } - incognitoBtn.icon?.alpha = 30 - incognitoBtn.isEnabled = true - Toast.makeText(requireContext(), "${getString(R.string.incognito)}: ${getString(R.string.disabled)}", Toast.LENGTH_SHORT).show() - }else{ - incognitoBtn.isEnabled = false - withContext(Dispatchers.IO) { - downloadViewModel.updateProcessingIncognito(true) - withContext(Dispatchers.Main){ - } - } - incognitoBtn.icon?.alpha = 255 - incognitoBtn.isEnabled = true - Toast.makeText(requireContext(), "${getString(R.string.incognito)}: ${getString(R.string.ok)}", Toast.LENGTH_SHORT).show() - } - } - } - R.id.folder -> { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - pathResultLauncher.launch(intent) - } R.id.preferred_download_type -> { lifecycleScope.launch{ val bottomSheet = BottomSheetDialog(requireContext()) @@ -416,7 +414,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure audio!!.setOnClickListener { CoroutineScope(Dispatchers.IO).launch { - downloadViewModel.updateProcessingType(DownloadViewModel.Type.audio) + downloadViewModel.updateProcessingType(listAdapter.getCheckedItemsOrNull(), DownloadViewModel.Type.audio) withContext(Dispatchers.Main){ preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24) formatBtn.icon?.alpha = 255 @@ -424,7 +422,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure bottomSheet.cancel() } - val res = downloadViewModel.checkIfAllProcessingItemsHaveSameContainer() + val res = downloadViewModel.checkIfAllProcessingItemsHaveSameContainer(listAdapter.getCheckedItemsOrNull()) withContext(Dispatchers.Main) { if (!res.first) { setContainerText("") @@ -438,7 +436,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure video!!.setOnClickListener { CoroutineScope(Dispatchers.IO).launch{ - downloadViewModel.updateProcessingType(DownloadViewModel.Type.video) + downloadViewModel.updateProcessingType(listAdapter.getCheckedItemsOrNull(), DownloadViewModel.Type.video) withContext(Dispatchers.Main){ preferredDownloadType.setIcon(R.drawable.baseline_video_file_24) formatBtn.icon?.alpha = 255 @@ -446,7 +444,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure bottomSheet.cancel() } - val res = downloadViewModel.checkIfAllProcessingItemsHaveSameContainer() + val res = downloadViewModel.checkIfAllProcessingItemsHaveSameContainer(listAdapter.getCheckedItemsOrNull()) withContext(Dispatchers.Main) { if (!res.first) { setContainerText("") @@ -459,7 +457,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure command!!.setOnClickListener { CoroutineScope(Dispatchers.IO).launch{ - downloadViewModel.updateProcessingType(DownloadViewModel.Type.command) + downloadViewModel.updateProcessingType(listAdapter.getCheckedItemsOrNull(), DownloadViewModel.Type.command) withContext(Dispatchers.Main){ preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24) formatBtn.icon?.alpha = 255 @@ -485,7 +483,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure R.id.format -> { lifecycleScope.launch { val res = withContext(Dispatchers.IO){ - downloadViewModel.checkIfAllProcessingItemsHaveSameType() + downloadViewModel.checkIfAllProcessingItemsHaveSameType(listAdapter.getCheckedItemsOrNull()) } if (!res.first){ Toast.makeText(requireContext(), getString(R.string.format_filtering_hint), Toast.LENGTH_SHORT).show() @@ -505,13 +503,13 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure lifecycleScope.launch { withContext(Dispatchers.IO){ - downloadViewModel.updateProcessingCommandFormat(format) + downloadViewModel.updateProcessingCommandFormat(listAdapter.getCheckedItemsOrNull(), format) } } } }else{ val items = withContext(Dispatchers.IO){ - downloadViewModel.getProcessingDownloads() + downloadViewModel.getProcessingDownloads(listAdapter.getCheckedItemsOrNull()) } formatViewModel.setItems(items) val bottomSheet = FormatSelectionBottomSheetDialog( _multipleFormatsListener = formatListener) @@ -520,10 +518,63 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure } } } + R.id.folder -> { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + pathResultLauncher.launch(intent) + } + R.id.select_items -> { + if (listAdapter.isCheckingItems()) { + multipleSelectHeader.isVisible = false + view.findViewById(R.id.downloadHeader).isVisible = true + filesize.isVisible = itemsFileSize > 0L + selectRangeBtn.isVisible = false + count.text = "${currentDownloadIDs.size} ${getString(R.string.selected)}" + listAdapter.clearCheckedItems() + }else { + multipleSelectHeader.apply { + isVisible = true + } + view.findViewById(R.id.downloadHeader).isVisible = false + filesize.isVisible = false + listAdapter.initCheckingItems(currentDownloadIDs) + selectRangeBtn.isVisible = true + count.text = "0 ${getString(R.string.selected)}" + } + } + R.id.incognito -> { + lifecycleScope.launch { + if (m.icon!!.alpha == 255) { + incognitoBtn.isEnabled = false + withContext(Dispatchers.IO) { + downloadViewModel.updateProcessingIncognito(listAdapter.getCheckedItemsOrNull(), false) + withContext(Dispatchers.Main){ + m.icon!!.alpha = 30 + m.isEnabled = true + } + } + incognitoBtn.icon?.alpha = 30 + incognitoBtn.isEnabled = true + Toast.makeText(requireContext(), "${getString(R.string.incognito)}: ${getString(R.string.disabled)}", Toast.LENGTH_SHORT).show() + }else{ + incognitoBtn.isEnabled = false + withContext(Dispatchers.IO) { + downloadViewModel.updateProcessingIncognito(listAdapter.getCheckedItemsOrNull(),true) + withContext(Dispatchers.Main){ + } + } + incognitoBtn.icon?.alpha = 255 + incognitoBtn.isEnabled = true + Toast.makeText(requireContext(), "${getString(R.string.incognito)}: ${getString(R.string.ok)}", Toast.LENGTH_SHORT).show() + } + } + } R.id.more -> { lifecycleScope.launch { val res = withContext(Dispatchers.IO){ - downloadViewModel.checkIfAllProcessingItemsHaveSameType() + downloadViewModel.checkIfAllProcessingItemsHaveSameType(listAdapter.getCheckedItemsOrNull()) } if (!res.first) { Toast.makeText( @@ -544,7 +595,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure sheetView.findViewById(R.id.adjust).setPadding(padding) val items = withContext(Dispatchers.IO){ - downloadViewModel.getProcessingDownloads() + downloadViewModel.getProcessingDownloads(listAdapter.getCheckedItemsOrNull()) } UiUtil.configureAudio( @@ -626,7 +677,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure sheetView.findViewById(R.id.adjust).setPadding(padding) val items = withContext(Dispatchers.IO){ - downloadViewModel.getProcessingDownloads() + downloadViewModel.getProcessingDownloads(listAdapter.getCheckedItemsOrNull()) } UiUtil.configureVideo( @@ -731,7 +782,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure containerTextView.setOnClickListener { lifecycleScope.launch { val res = withContext(Dispatchers.IO){ - downloadViewModel.checkIfAllProcessingItemsHaveSameType() + downloadViewModel.checkIfAllProcessingItemsHaveSameType(listAdapter.getCheckedItemsOrNull()) } if (!res.first){ Toast.makeText(requireContext(), getString(R.string.format_filtering_hint), Toast.LENGTH_SHORT).show() @@ -749,7 +800,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure val container = mm.title lifecycleScope.launch { withContext(Dispatchers.IO){ - downloadViewModel.updateProcessingContainer(container.toString()) + downloadViewModel.updateProcessingContainer(listAdapter.getCheckedItemsOrNull(), container.toString()) } } setContainerText(container.toString()) @@ -758,9 +809,90 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure popup.show() } + } + } + + multipleSelectHeader = view.findViewById(R.id.multipleSelectHeader) + selectItemsMenuBtn = view.findViewById(R.id.selectItemsMenu) + selectItemsMenuBtn.setOnClickListener { + val popup = PopupMenu(activity, it) + popup.menuInflater.inflate(R.menu.select_multiple_items_menu_context, popup.menu) + if (Build.VERSION.SDK_INT > 27) popup.menu.setGroupDividerEnabled(true) + + val selectedItems = listAdapter.getCheckedItemsOrNull() ?: listOf() + popup.menu.findItem(R.id.delete).isVisible = selectedItems.isNotEmpty() + popup.menu.findItem(R.id.select_between).apply { + if (selectedItems.size == 2) { + val firstIndex = currentDownloadIDs.indexOf(selectedItems.first()) + val secondIndex = currentDownloadIDs.indexOf(selectedItems.last()) + + isVisible = abs(firstIndex - secondIndex) > 1 + }else{ + isVisible = false + } + } + popup.setOnMenuItemClickListener { m: MenuItem -> + when(m.itemId) { + R.id.select_between -> { + val firstIndex = currentDownloadIDs.indexOf(selectedItems.first()) + val secondIndex = currentDownloadIDs.indexOf(selectedItems.last()) + val itemsBetween = currentDownloadIDs.filterIndexed { index, _ -> index >= firstIndex && index <= secondIndex } + listAdapter.selectItems(itemsBetween) + count.text = "${itemsBetween.size} ${getString(R.string.selected)}" + } + R.id.delete -> { + UiUtil.showGenericDeleteAllDialog(requireContext()) { + lifecycleScope.launch { + val deletedAll = processingItemsCount == listAdapter.getCheckedItemsSize() + val toDelete = listAdapter.getCheckedItemsOrNull() ?: listOf() + withContext(Dispatchers.IO) { + downloadViewModel.deleteAllWithID(toDelete) + } + listAdapter.removeItemsFromCheckList(toDelete) + val checkedSize = listAdapter.getCheckedItemsSize() + count.text = "$checkedSize ${getString(R.string.selected)}" + if (deletedAll) dismiss() + } + } + } + R.id.select_all -> { + listAdapter.checkAll() + val checkedSize = listAdapter.getCheckedItemsSize() + count.text = "$checkedSize ${getString(R.string.selected)}" + } + R.id.invert_selected -> { + listAdapter.invertSelected() + val checkedSize = listAdapter.getCheckedItemsSize() + count.text = "$checkedSize ${getString(R.string.selected)}" + } + } + true } + + popup.show() + } + + selectRangeBtn = view.findViewById(R.id.selectRangeBtn) + selectRangeBtn.setOnClickListener { + UiUtil.showSelectRangeDialog(requireActivity(), currentDownloadIDs.size) { + val itemsBetween = currentDownloadIDs.filterIndexed { index, _ -> index >= it.first && index <= it.second } + listAdapter.selectItems(itemsBetween) + selectItemsMenuBtn.isVisible = true + count.text = "${itemsBetween.size} ${getString(R.string.selected)}" + } + } + + val editSelectedOkBtn = view.findViewById(R.id.bottomsheet_ok_button) + editSelectedOkBtn.setOnClickListener { + multipleSelectHeader.isVisible = false + view.findViewById(R.id.downloadHeader).isVisible = true + selectItemsMenuBtn.isVisible = false + filesize.isVisible = itemsFileSize > 0 + count.text = "${currentDownloadIDs.size} ${getString(R.string.selected)}" + selectRangeBtn.isVisible = false + listAdapter.clearCheckedItems() } } @@ -819,7 +951,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure if (fileSizes.all { it > 5L }){ val size = FileUtil.convertFileSize(fileSizes.sum()) - filesize.isVisible = size != "?" + itemsFileSize = fileSizes.sum() + filesize.isVisible = size != "?" && !listAdapter.isCheckingItems() filesize.text = "${getString(R.string.file_size)}: >~ $size" }else{ filesize.visibility = View.GONE @@ -839,7 +972,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure } CoroutineScope(Dispatchers.IO).launch { - downloadViewModel.updateProcessingDownloadPath(result.data?.data.toString()) + downloadViewModel.updateProcessingDownloadPath(listAdapter.getCheckedItemsOrNull(), result.data?.data.toString()) } val path = FileUtil.formatPath(result.data!!.data.toString()) @@ -926,6 +1059,20 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure } } + override fun onCardChecked(id: Long) { + selectItemsMenuBtn.isVisible = true + val checkedSize = listAdapter.getCheckedItemsSize() + count.text = "$checkedSize ${getString(R.string.selected)}" + } + + override fun onCardUnChecked(id: Long) { + val checkedSize = listAdapter.getCheckedItemsSize() + if (checkedSize == 0) { + selectItemsMenuBtn.isVisible = false + } + count.text = "$checkedSize ${getString(R.string.selected)}" + } + override fun onDelete(id: Long) { lifecycleScope.launch { val deletedItem = withContext(Dispatchers.IO){ diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt index b82def6c..3e8666b6 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -20,6 +20,7 @@ import android.os.Environment import android.text.Editable import android.text.TextWatcher import android.text.format.DateFormat +import android.text.method.DigitsKeyListener import android.text.method.LinkMovementMethod import android.util.DisplayMetrics import android.view.Gravity @@ -2014,6 +2015,73 @@ object UiUtil { dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START } + fun showSelectRangeDialog(context: Activity, itemCount: Int, rangeSelected: (rangeSelected: Pair) -> Unit) { + val builder = MaterialAlertDialogBuilder(context) + builder.setTitle(context.getString(R.string.select_between)) + builder.setMessage(context.getString(R.string.select_between_desc)) + builder.setIcon(R.drawable.baseline_format_list_numbered_24) + val view = context.layoutInflater.inflate(R.layout.select_range_dialog, null) + + val fromTextInput = view.findViewById(R.id.from_textinput) + fromTextInput.editText!!.keyListener = DigitsKeyListener.getInstance("0123456789") + val toTextInput = view.findViewById(R.id.to_textinput) + toTextInput.editText!!.keyListener = DigitsKeyListener.getInstance("0123456789") + + + builder.setView(view) + builder.setPositiveButton( + context.getString(R.string.ok) + ) { _: DialogInterface?, _: Int -> + val firstIndex = fromTextInput.editText!!.text.toString().toInt() - 1 + val secondIndex = toTextInput.editText!!.text.toString().toInt() - 1 + + rangeSelected(Pair(firstIndex,secondIndex)) + } + + // handle the negative button of the alert dialog + builder.setNegativeButton( + context.getString(R.string.cancel) + ) { _: DialogInterface?, _: Int -> } + + val dialog = builder.create() + dialog.show() + + + fun checkRanges(start: String, end: String) : Boolean { + fromTextInput.error = "" + toTextInput.error = "" + + if (start.isBlank() || end.isBlank()) return false + + val startValid = start.toInt() >= 0 + val endValid = end.toInt() <= itemCount + + if (!startValid) { + fromTextInput.editText?.setText("") + fromTextInput.error = "Invalid Number" + } + if (!endValid) { + toTextInput.editText?.setText("") + toTextInput.error = "Invalid Number" + } + + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = startValid && endValid + return startValid && endValid + } + + fromTextInput.editText!!.doAfterTextChanged { editable -> + val start = editable.toString() + val end = toTextInput.editText!!.text.toString() + checkRanges(start, end) + } + + toTextInput.editText!!.doAfterTextChanged { editable -> + val start = fromTextInput.editText!!.text.toString() + val end = editable.toString() + checkRanges(start, end) + } + } + private fun createPersonalFilenameTemplateChip(context: Activity, text: String, myChipGroup: ChipGroup, onClick: (f: Chip) -> Unit, onLongClick: (f: Chip) -> Unit) : Chip { val tmp = context.layoutInflater.inflate(R.layout.filter_chip, myChipGroup, false) as Chip tmp.text = text diff --git a/app/src/main/res/drawable/baseline_format_list_numbered_24.xml b/app/src/main/res/drawable/baseline_format_list_numbered_24.xml new file mode 100644 index 00000000..d7e8c74d --- /dev/null +++ b/app/src/main/res/drawable/baseline_format_list_numbered_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_select_all.xml b/app/src/main/res/drawable/ic_select_all.xml index af331ce6..b45a45f7 100644 --- a/app/src/main/res/drawable/ic_select_all.xml +++ b/app/src/main/res/drawable/ic_select_all.xml @@ -1,4 +1,4 @@ - diff --git a/app/src/main/res/drawable/rounded_corner_index.xml b/app/src/main/res/drawable/rounded_corner_index.xml new file mode 100644 index 00000000..0a88da97 --- /dev/null +++ b/app/src/main/res/drawable/rounded_corner_index.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/download_card.xml b/app/src/main/res/layout/download_card.xml index 2a8d0739..44ead077 100644 --- a/app/src/main/res/layout/download_card.xml +++ b/app/src/main/res/layout/download_card.xml @@ -21,25 +21,57 @@ + + + + + - + android:visibility="gone" + android:layout_height="wrap_content" + android:layout_marginHorizontal="20dp" + android:orientation="horizontal" + android:paddingTop="20dp"> + + +