From 1426ceef0a092d6962a538bc5a9937ac36c51825 Mon Sep 17 00:00:00 2001 From: deniscerri <64997243+deniscerri@users.noreply.github.com> Date: Sat, 29 Mar 2025 22:35:39 +0100 Subject: [PATCH] more fixes --- .../database/models/FormatRecyclerView.kt | 10 + .../database/repository/ResultRepository.kt | 7 +- .../database/viewmodel/DownloadViewModel.kt | 10 +- .../database/viewmodel/FormatViewModel.kt | 248 +++++++ .../database/viewmodel/ResultViewModel.kt | 6 +- .../ConfigureMultipleDownloadsAdapter.kt | 2 +- .../ytdl/ui/adapter/FormatAdapter.kt | 112 +-- .../ui/downloadcard/DownloadAudioFragment.kt | 6 +- .../DownloadMultipleBottomSheetDialog.kt | 6 +- .../ui/downloadcard/DownloadVideoFragment.kt | 6 +- .../FormatSelectionBottomSheetDialog.kt | 694 +++++------------- .../more/settings/GeneralSettingsFragment.kt | 11 +- .../GenerateYoutubePoTokensFragment.kt | 64 +- .../webview/PoTokenWebViewLoginActivity.kt | 17 +- .../java/com/deniscerri/ytdl/util/UiUtil.kt | 1 + app/src/main/res/layout/download_card.xml | 2 +- .../main/res/layout/format_category_sheet.xml | 50 -- .../res/layout/format_select_bottom_sheet.xml | 74 +- app/src/main/res/layout/format_type_label.xml | 15 + .../generate_po_token_url_bottom_sheet.xml | 57 ++ .../main/res/menu/history_menu_context.xml | 2 +- app/src/main/res/values/arrays.xml | 2 + app/src/main/res/values/strings.xml | 3 +- app/src/main/res/xml/general_preferences.xml | 9 +- 24 files changed, 735 insertions(+), 679 deletions(-) create mode 100644 app/src/main/java/com/deniscerri/ytdl/database/models/FormatRecyclerView.kt create mode 100644 app/src/main/java/com/deniscerri/ytdl/database/viewmodel/FormatViewModel.kt create mode 100644 app/src/main/res/layout/format_type_label.xml create mode 100644 app/src/main/res/layout/generate_po_token_url_bottom_sheet.xml diff --git a/app/src/main/java/com/deniscerri/ytdl/database/models/FormatRecyclerView.kt b/app/src/main/java/com/deniscerri/ytdl/database/models/FormatRecyclerView.kt new file mode 100644 index 00000000..b6a56a5d --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/database/models/FormatRecyclerView.kt @@ -0,0 +1,10 @@ +package com.deniscerri.ytdl.database.models + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +data class FormatRecyclerView( + var label: String? = null, + var format: Format? = null, +) \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt index caac3e17..8cb05867 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt @@ -57,7 +57,7 @@ class ResultRepository(private val resultDao: ResultDao, private val commandTemp suspend fun getHomeRecommendations(){ deleteAll() - val category = sharedPreferences.getString("youtube_home_recommendations", "") + val category = sharedPreferences.getString("home_recommendations", "") val items = when(category) { "newpipe" -> newPipeUtil.getTrending() "yt_api" -> youtubeApiUtil.getTrending() @@ -65,6 +65,11 @@ class ResultRepository(private val resultDao: ResultDao, private val commandTemp "yt_dlp_recommendations" -> ytdlpUtil.getYoutubeRecommendations() "yt_dlp_liked" -> ytdlpUtil.getYoutubeLikedVideos() "yt_dlp_watch_history" -> ytdlpUtil.getYoutubeWatchHistory() + "custom" -> { + val customURL = sharedPreferences.getString("custom_home_recommendation_url", "") + if (customURL.isNullOrBlank()) arrayListOf() + else ytdlpUtil.getFromYTDL(customURL) + } else -> arrayListOf() } 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 9eea3b26..96daf7b1 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 @@ -1099,8 +1099,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } if (it.type == Type.video) { + it.videoPreferences.audioFormatIDs.clear() ft.audioFormats?.map { a -> a.format_id }?.let { list -> - it.videoPreferences.audioFormatIDs.clear() it.videoPreferences.audioFormatIDs.addAll(list) } } @@ -1342,8 +1342,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel cancelAllDownloadsImpl() } - private fun cancelAllDownloadsImpl() { + private suspend fun cancelAllDownloadsImpl() { WorkManager.getInstance(application).cancelAllWorkByTag("download") + val activeDownloadsList = withContext(Dispatchers.IO){ + getActiveDownloads() + } + activeDownloadsList.forEach { + cancelDownloadOnly(it.id) + } cancelActiveQueued() } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/FormatViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/FormatViewModel.kt new file mode 100644 index 00000000..6e4e8e7b --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/FormatViewModel.kt @@ -0,0 +1,248 @@ +package com.deniscerri.ytdl.database.viewmodel + +import android.app.Application +import android.view.View +import android.widget.Toast +import androidx.compose.runtime.MutableState +import androidx.core.view.isVisible +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewModelScope +import androidx.preference.PreferenceManager +import com.afollestad.materialdialogs.utils.MDUtil.getStringArray +import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.database.DBManager +import com.deniscerri.ytdl.database.models.DownloadItem +import com.deniscerri.ytdl.database.models.Format +import com.deniscerri.ytdl.database.models.FormatRecyclerView +import com.deniscerri.ytdl.database.repository.DownloadRepository +import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type +import com.deniscerri.ytdl.ui.downloadcard.FormatSelectionBottomSheetDialog.FormatCategory +import com.deniscerri.ytdl.ui.downloadcard.FormatSelectionBottomSheetDialog.FormatSorting +import com.deniscerri.ytdl.ui.downloadcard.FormatTuple +import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple +import com.deniscerri.ytdl.util.Extensions.isYoutubeURL +import com.deniscerri.ytdl.util.FormatUtil +import com.deniscerri.ytdl.util.UiUtil +import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.text.Normalizer.Form + +class FormatViewModel(private val application: Application) : AndroidViewModel(application) { + private val downloadRepository: DownloadRepository + val selectedItems = MutableStateFlow(listOf()) + val selectedItemsSharedFlow = MutableSharedFlow>(replay = 1) + var formats : Flow> + var showFilterBtn = MutableStateFlow(false) + var showRefreshBtn = MutableStateFlow(false) + private var canUpdate = true + var canMultiSelectAudio = MutableStateFlow(false) + var isMissingFormats = MutableStateFlow(false) + + private var formatUtil = FormatUtil(application) + private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) + + val genericAudioFormats = formatUtil.getGenericAudioFormats(application.resources) + val genericVideoFormats = formatUtil.getGenericVideoFormats(application.resources) + + var sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!) + var filterBy = MutableStateFlow(FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!)) + + init { + downloadRepository = DownloadRepository(DBManager.getInstance(application).downloadDao) + formats = combine(listOf(selectedItemsSharedFlow, filterBy)) { f -> + val items = selectedItems.value + + if (items.isEmpty()) { + mutableListOf() + }else { + val formats = if (items.size == 1) { + items.first().allFormats + }else { + val flatFormatCollection = items.map { it.allFormats }.flatten() + + flatFormatCollection.groupingBy { it.format_id }.eachCount() + .filter { it.value == items.size } + .mapValues { flatFormatCollection.first { f -> f.format_id == it.key } } + .map { it.value }.toMutableList() + } + + isMissingFormats.apply { + val vl = formats.isEmpty() + value = vl + emit(vl) + } + + var chosenFormats: List + + if (items.size > 1) { + if (!isMissingFormats.value) { + chosenFormats = formats.mapTo(mutableListOf()) {it.copy()} + chosenFormats = when(items.first().type){ + Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) } + else -> chosenFormats + } + chosenFormats.forEach { + it.filesize = items.map { itm -> itm.allFormats }.flatten().filter { f -> f.format_id == it.format_id }.sumOf { itt -> itt.filesize } + } + }else{ + chosenFormats = formats + } + }else{ + chosenFormats = formats + if(!isMissingFormats.value){ + if(items.first().type == Type.audio){ + chosenFormats = chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) } + } + } + } + + showFilterBtn.apply { + val vl = chosenFormats.isNotEmpty() || items.all { it.url.isYoutubeURL() } + value = vl + emit(vl) + } + showRefreshBtn.apply { + val vl = (isMissingFormats.value || items.isEmpty() || items.first().url.isEmpty()) && canUpdate + value = vl + emit(vl) + } + + //sort + var finalFormats: List = when(sortBy){ + FormatSorting.container -> chosenFormats.groupBy { it.container }.flatMap { it.value } + FormatSorting.id -> chosenFormats.sortedBy { it.format_id } + FormatSorting.codec -> { + val codecOrder = application.getStringArray(R.array.video_codec_values).toMutableList() + codecOrder.removeAt(0) + chosenFormats.groupBy { format -> codecOrder.indexOfFirst { format.vcodec.matches("^(${it})(.+)?$".toRegex()) } } + + .flatMap { + it.value.sortedByDescending { l -> l.filesize } + } + } + FormatSorting.filesize -> chosenFormats + } + + //filter category + when(filterBy.value){ + FormatCategory.ALL -> {} + FormatCategory.SUGGESTED -> { + finalFormats = if (items.first().type == Type.audio){ + formatUtil.sortAudioFormats(finalFormats) + }else{ + val audioFormats = finalFormats.filter { it.vcodec.isBlank() || it.vcodec == "none" } + val videoFormats = finalFormats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" } + + formatUtil.sortVideoFormats(videoFormats) + formatUtil.sortAudioFormats(audioFormats) + } + } + FormatCategory.SMALLEST -> { + val tmpFormats = finalFormats + .asSequence() + .map { it.copy() } + .filter { it.filesize > 0 } + .onEach { + var tmp = it.format_note.lowercase() + //remove brackets + tmp = tmp.replace(" (.*)".toRegex(), "") + + //formats that end like 1080P + if (tmp.endsWith("060")){ + tmp = tmp.removeSuffix("60") + } + tmp = tmp.removeSuffix("p") + //formats that are written like 1920X1080 + val split = tmp.split("x") + if (split.size > 1){ + tmp = split[1] + } + it.format_note = tmp + } + .groupBy { it.format_note } + .map { it.value.minBy { it2 -> it2.filesize } }.toList() + finalFormats = finalFormats.filter { tmpFormats.map { it2 -> it2.format_id }.contains(it.format_id) } + } + FormatCategory.GENERIC -> { + finalFormats = listOf() + } + } + + canMultiSelectAudio.apply { + val vl = items.first().type == Type.video && finalFormats.find { it.vcodec.isBlank() || it.vcodec == "none" } != null + value = vl + emit(vl) + } + + if (finalFormats.isEmpty()) { + finalFormats = if (items.first().type == Type.audio){ + genericAudioFormats + }else{ + genericVideoFormats + } + } + + + val results = mutableListOf() + if (canMultiSelectAudio.value) { + results.add(FormatRecyclerView(label = application.getString(R.string.video))) + results.addAll(finalFormats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.map { FormatRecyclerView(null, it) }) + results.add(FormatRecyclerView(label = application.getString(R.string.audio))) + results.addAll(finalFormats.filter { it.vcodec.isBlank() || it.vcodec == "none" }.map { FormatRecyclerView(null, it) }) + }else{ + results.addAll(finalFormats.map { FormatRecyclerView(null, it) }) + } + + results + } + } + } + + fun setItems(list: List, updateFormats: Boolean? = null) = viewModelScope.launch { + selectedItems.apply { + value = list + emit(list) + } + selectedItemsSharedFlow.emit(list) + canUpdate = updateFormats ?: canUpdate + } + + fun setItem(item: DownloadItem, updateFormats: Boolean? = null) = viewModelScope.launch { + selectedItems.apply { + value = listOf(item) + emit(listOf(item)) + } + selectedItemsSharedFlow.emit(listOf(item)) + canUpdate = updateFormats ?: canUpdate + } + + + fun getFormatsForItemsBasedOnFormat(item: Format, audioFormats: List? = null) : MutableList { + val formatsToReturn = mutableListOf() + val f = if (genericAudioFormats.contains(item) || genericVideoFormats.contains(item)) item else null + + selectedItems.value.forEach { + formatsToReturn.add( + MultipleItemFormatTuple( + it.url, + FormatTuple( + f ?: it.allFormats.firstOrNull { af -> af.format_id == item.format_id }, + audioFormats?.map { sa -> + it.allFormats.first { a -> a.format_id == sa.format_id } + }?.ifEmpty { null } + ) + ) + ) + } + + return formatsToReturn + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt index 6ffdbe40..c6a261ae 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt @@ -124,7 +124,11 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a } } fun getHomeRecommendations() = viewModelScope.launch(Dispatchers.IO){ - if (!sharedPreferences.getString("youtube_home_recommendations", "").isNullOrBlank()){ + val homeRecommendations = sharedPreferences.getString("home_recommendations", "") + val customHomeRecommendations = sharedPreferences.getString("custom_home_recommendation_url", "") + val emptyCustomRecommendations = customHomeRecommendations.isNullOrBlank() && homeRecommendations == "custom" + + if (!homeRecommendations.isNullOrBlank() && !emptyCustomRecommendations){ kotlin.runCatching { uiState.update {it.copy(processing = true)} repository.getHomeRecommendations() 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 f71c56e8..282fdc11 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 @@ -102,7 +102,7 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener } else { item.format.acodec.uppercase() } - if (codecText == "" || codecText == "none"){ + if (codecText == "" || codecText == "none" || codecText == "DEFAULT"){ codec.visibility = View.GONE }else{ codec.visibility = View.VISIBLE diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/FormatAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/FormatAdapter.kt index a1a78d7f..22eac194 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/FormatAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/FormatAdapter.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Button import android.widget.TextView import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil @@ -12,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.CookieItem import com.deniscerri.ytdl.database.models.Format +import com.deniscerri.ytdl.database.models.FormatRecyclerView import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.databinding.FormatItemBinding import com.deniscerri.ytdl.ui.adapter.HistoryPaginatedAdapter.ViewHolder @@ -19,39 +21,54 @@ import com.deniscerri.ytdl.util.Extensions.popup import com.deniscerri.ytdl.util.UiUtil import com.google.android.material.card.MaterialCardView -class FormatAdapter(onItemClickListener: OnItemClickListener, activity: Activity, private val downloadType: DownloadViewModel.Type) : ListAdapter(AsyncDifferConfig.Builder( +class FormatAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter(AsyncDifferConfig.Builder( DIFF_CALLBACK ).build()) { private val onItemClickListener: OnItemClickListener private val activity: Activity - private var selectedVideoFormat: Format? - private val selectedAudioFormats: MutableList - private val usingGrid: Boolean + var selectedVideoFormat: Format? = null + val selectedAudioFormats: MutableList = mutableListOf() + private var canMultiSelectAudio: Boolean = false + private var formats: MutableList = mutableListOf() init { this.onItemClickListener = onItemClickListener this.activity = activity - this.selectedVideoFormat = null - this.usingGrid = false - this.selectedAudioFormats = mutableListOf() } class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { - val item: MaterialCardView + val item: MaterialCardView? = itemView.findViewById(R.id.format_card_constraintLayout) + val label: Button? = itemView.findViewById(R.id.title) + } - init { - item = itemView.findViewById(R.id.format_card_constraintLayout) + override fun submitList(list: MutableList?) { + if (list != null) { + formats = list + } + super.submitList(list) + } + + fun setCanMultiSelectAudio(it: Boolean) { + canMultiSelectAudio = it + } + + override fun getItemViewType(position: Int): Int { + try { + val isLabel = formats[position]!!.label != null + return if (isLabel) 0 else 1 + }catch (err: Exception) { + return 1 } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return if (usingGrid){ - val cardView = LayoutInflater.from(parent.context) - .inflate(R.layout.format_item_grid, parent, false) + return if (viewType == 0){ + val button = LayoutInflater.from(parent.context) + .inflate(R.layout.format_type_label, parent, false) ViewHolder( - cardView, + button, onItemClickListener ) }else{ @@ -67,54 +84,65 @@ class FormatAdapter(onItemClickListener: OnItemClickListener, activity: Activity override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val item = getItem(position) ?: return - val card = holder.item - card.popup() + val itm = getItem(position) ?: return + val viewType = getItemViewType(position) + if (viewType == 0) { + val button = holder.label + button?.text = itm.label + return + } + + val item = itm.format!! + val card = holder.item!! + //card.popup() UiUtil.populateFormatCard(activity, card, item) + card.isChecked = selectedVideoFormat == item || selectedAudioFormats.any { it == item } + card.setOnClickListener { - when(downloadType) { - DownloadViewModel.Type.audio -> { - onItemClickListener.onItemClick(item) - } - DownloadViewModel.Type.video -> { - if(item.isAudio()) { - if (card.isChecked) { - selectedAudioFormats.remove(item) - }else { - selectedAudioFormats.add(item) - } + if (!canMultiSelectAudio) { + onItemClickListener.onItemSelect(item, null) + }else { + if (item.isVideo()) { + if (card.isChecked) { + onItemClickListener.onItemSelect(item, selectedAudioFormats) }else { - if (card.isChecked) { - onItemClickListener.onItemClick(item) - }else { - selectedVideoFormat = item - card.isChecked = true - } + selectedVideoFormat = item + notifyDataSetChanged() } + }else { + if (card.isChecked) { + selectedAudioFormats.remove(item) + }else { + selectedAudioFormats.add(item) + } + notifyDataSetChanged() } - else -> {} } } + card.setOnLongClickListener { + UiUtil.showFormatDetails(item, activity) + true + } } - private fun Format.isAudio() : Boolean { + private fun Format.isVideo() : Boolean { return this.vcodec.isNotBlank() && this.vcodec != "none" } interface OnItemClickListener { - fun onItemClick(item: Format) + fun onItemSelect(item: Format, audioFormats: List?) } companion object { - private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Format, newItem: Format): Boolean { - return oldItem.format_id == newItem.format_id + private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: FormatRecyclerView, newItem: FormatRecyclerView): Boolean { + return oldItem.label == newItem.label && oldItem.format?.format_id == newItem.format?.format_id } - override fun areContentsTheSame(oldItem: Format, newItem: Format): Boolean { - return oldItem.format_id == newItem.format_id + override fun areContentsTheSame(oldItem: FormatRecyclerView, newItem: FormatRecyclerView): Boolean { + return oldItem.label == newItem.label && oldItem.format?.format_id == newItem.format?.format_id } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadAudioFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadAudioFragment.kt index a3942412..f94758b3 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadAudioFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadAudioFragment.kt @@ -30,6 +30,7 @@ import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.ResultItem import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type +import com.deniscerri.ytdl.database.viewmodel.FormatViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.util.Extensions.applyFilenameTemplateForCuts import com.deniscerri.ytdl.util.FileUtil @@ -54,6 +55,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private private var activity: Activity? = null private lateinit var downloadViewModel : DownloadViewModel private lateinit var resultViewModel : ResultViewModel + private lateinit var formatViewModel : FormatViewModel private lateinit var saveDir : TextInputLayout private lateinit var freeSpace : TextView private lateinit var genericAudioFormats: MutableList @@ -73,6 +75,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private activity = getActivity() downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] + formatViewModel = ViewModelProvider(requireActivity())[FormatViewModel::class.java] genericAudioFormats = FormatUtil(requireContext()).getGenericAudioFormats(requireContext().resources) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) shownFields = preferences.getStringSet("modify_download_card", requireContext().getStringArray(R.array.modify_download_card_values).toSet())!!.toList() @@ -252,7 +255,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private } formatCard.setOnClickListener{ if (parentFragmentManager.findFragmentByTag("formatSheet") == null){ - val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listener, canUpdate = !nonSpecific) + formatViewModel.setItem(downloadItem, !nonSpecific) + val bottomSheet = FormatSelectionBottomSheetDialog(listener) bottomSheet.show(parentFragmentManager, "formatSheet") } } 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 d155013b..86495078 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 @@ -37,6 +37,7 @@ import com.deniscerri.ytdl.database.models.DownloadItemConfigureMultiple import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel +import com.deniscerri.ytdl.database.viewmodel.FormatViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.receiver.ShareActivity @@ -70,6 +71,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure private lateinit var historyViewModel: HistoryViewModel private lateinit var commandTemplateViewModel: CommandTemplateViewModel private lateinit var resultViewModel: ResultViewModel + private lateinit var formatViewModel: FormatViewModel private lateinit var listAdapter : ConfigureMultipleDownloadsAdapter private lateinit var recyclerView: RecyclerView private lateinit var behavior: BottomSheetBehavior @@ -97,6 +99,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java] resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java] + formatViewModel = ViewModelProvider(requireActivity())[FormatViewModel::class.java] commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java] sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) @@ -506,7 +509,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure val items = withContext(Dispatchers.IO){ downloadViewModel.getProcessingDownloads() } - val bottomSheet = FormatSelectionBottomSheetDialog(items, _multipleFormatsListener = formatListener) + formatViewModel.setItems(items) + val bottomSheet = FormatSelectionBottomSheetDialog( _multipleFormatsListener = formatListener) bottomSheet.show(parentFragmentManager, "formatSheet") } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadVideoFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadVideoFragment.kt index 6e61c74b..290695b7 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadVideoFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadVideoFragment.kt @@ -30,6 +30,7 @@ import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.ResultItem import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type +import com.deniscerri.ytdl.database.viewmodel.FormatViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.util.Extensions.applyFilenameTemplateForCuts import com.deniscerri.ytdl.util.FileUtil @@ -53,6 +54,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private private var fragmentView: View? = null private var activity: Activity? = null private lateinit var downloadViewModel : DownloadViewModel + private lateinit var formatViewModel : FormatViewModel private lateinit var resultViewModel: ResultViewModel private lateinit var preferences: SharedPreferences private lateinit var shownFields: List @@ -77,6 +79,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private activity = getActivity() downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] + formatViewModel = ViewModelProvider(requireActivity())[FormatViewModel::class.java] val formatUtil = FormatUtil(requireContext()) genericVideoFormats = formatUtil.getGenericVideoFormats(requireContext().resources) genericAudioFormats = formatUtil.getGenericAudioFormats(requireContext().resources) @@ -290,7 +293,8 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private } formatCard.setOnClickListener{ if (parentFragmentManager.findFragmentByTag("formatSheet") == null){ - val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listener, canUpdate = !nonSpecific) + formatViewModel.setItem(downloadItem, !nonSpecific) + val bottomSheet = FormatSelectionBottomSheetDialog(listener) bottomSheet.show(parentFragmentManager, "formatSheet") } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt index 433c3204..49835646 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt @@ -20,13 +20,19 @@ import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.Format +import com.deniscerri.ytdl.database.models.FormatRecyclerView import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type +import com.deniscerri.ytdl.database.viewmodel.FormatViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.ui.adapter.FormatAdapter +import com.deniscerri.ytdl.ui.adapter.HomeAdapter +import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.isYoutubeURL import com.deniscerri.ytdl.util.FormatUtil import com.deniscerri.ytdl.util.UiUtil @@ -39,17 +45,27 @@ import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.w3c.dom.Text class FormatSelectionBottomSheetDialog( - private val _items: List? = null, private val _listener: OnFormatClickListener? = null, - private val canUpdate: Boolean = true, private val _multipleFormatsListener: OnMultipleFormatClickListener? = null -) : BottomSheetDialogFragment() { +) : BottomSheetDialogFragment(), FormatAdapter.OnItemClickListener { + + private lateinit var formatViewModel: FormatViewModel + private lateinit var recyclerView: RecyclerView + private lateinit var adapter: FormatAdapter + private lateinit var genericAudioFormats : List + private lateinit var genericVideoFormats : List + private var formats: List = listOf() + + private var canMultiSelectAudio: Boolean = false private lateinit var behavior: BottomSheetBehavior private lateinit var formatUtil: FormatUtil @@ -58,36 +74,17 @@ class FormatSelectionBottomSheetDialog( private lateinit var downloadViewModel: DownloadViewModel private lateinit var resultViewModel: ResultViewModel private lateinit var sharedPreferences: SharedPreferences - private lateinit var videoFormatList : GridLayout - private lateinit var audioFormatList : GridLayout private lateinit var okBtn : Button private lateinit var refreshBtn: Button - private lateinit var videoTitle : TextView - private lateinit var audioTitle : TextView - - private lateinit var chosenFormats: List - private var selectedVideo : Format? = null - private lateinit var selectedAudios : MutableList - - private lateinit var sortBy : FormatSorting - private lateinit var filterBy : FormatCategory private lateinit var filterBtn : Button private var updateFormatsJob: Job? = null - private var isMissingFormats: Boolean = false - private lateinit var items: MutableList - private lateinit var formats: MutableList private lateinit var listener: OnFormatClickListener private lateinit var multipleFormatsListener: OnMultipleFormatClickListener private var currentFormatSource : String? = null - private lateinit var genericAudioFormats : List - private lateinit var genericVideoFormats : List - - private var usingGrid: Boolean = false - enum class FormatSorting { filesize, container, codec, id } @@ -98,9 +95,8 @@ class FormatSelectionBottomSheetDialog( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + formatViewModel = ViewModelProvider(requireActivity())[FormatViewModel::class.java] formatUtil = FormatUtil(requireContext()) - chosenFormats = listOf() - selectedAudios = mutableListOf() sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] @@ -113,20 +109,66 @@ class FormatSelectionBottomSheetDialog( view = LayoutInflater.from(context).inflate(R.layout.format_select_bottom_sheet, null) dialog.setContentView(view) - if (_items == null){ - this.dismiss() - return + dialog.setOnShowListener { + behavior = BottomSheetBehavior.from(view.parent as View) + val displayMetrics = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) + behavior.peekHeight = displayMetrics.heightPixels / 2 } - items = _items.toMutableList() - if (items.size == 1) { - formats = items.first()!!.allFormats - }else{ - val flatFormatCollection = items.map { it!!.allFormats }.flatten() - formats = flatFormatCollection.groupingBy { it.format_id }.eachCount() - .filter { it.value == items.size } - .mapValues { flatFormatCollection.first { f -> f.format_id == it.key } } - .map { it.value }.toMutableList() + view.findViewById(R.id.bottom_sheet_title).setOnClickListener { + recyclerView.scrollTo(0,0) + } + + genericAudioFormats = formatUtil.getGenericAudioFormats(resources) + genericVideoFormats = formatUtil.getGenericVideoFormats(resources) + + adapter = + FormatAdapter( + this, + requireActivity() + ) + recyclerView = view.findViewById(R.id.recyclerView) + recyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) + recyclerView.adapter = adapter + + refreshBtn = view.findViewById(R.id.format_refresh) + okBtn = view.findViewById(R.id.format_ok) + filterBtn = view.findViewById(R.id.format_filter) + val shimmers = view.findViewById(R.id.format_list_shimmer) + + lifecycleScope.launch { + formatViewModel.formats.collectLatest { + if (it.isEmpty()) { + this@FormatSelectionBottomSheetDialog.dismiss() + } + adapter.setCanMultiSelectAudio(formatViewModel.canMultiSelectAudio.value) + formats = it + adapter.submitList(it.toMutableList()) + shimmers.visibility = View.GONE + shimmers.stopShimmer() + recyclerView.isVisible = true + } + } + + + lifecycleScope.launch { + formatViewModel.showFilterBtn.collectLatest { + filterBtn.isVisible = it + } + } + + lifecycleScope.launch { + formatViewModel.showRefreshBtn.collectLatest { + refreshBtn.isVisible = it + } + } + + lifecycleScope.launch { + formatViewModel.canMultiSelectAudio.collectLatest { + okBtn.isVisible = it + canMultiSelectAudio = it + } } _listener?.apply { @@ -137,211 +179,119 @@ class FormatSelectionBottomSheetDialog( multipleFormatsListener = this } - genericAudioFormats = formatUtil.getGenericAudioFormats(requireContext().resources) - genericVideoFormats = formatUtil.getGenericVideoFormats(requireContext().resources) - - sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!) - filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!) - filterBtn = view.findViewById(R.id.format_filter) - usingGrid = sharedPreferences.getBoolean("format_list_grid", false) - - dialog.setOnShowListener { - behavior = BottomSheetBehavior.from(view.parent as View) - val displayMetrics = DisplayMetrics() - requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) - behavior.peekHeight = displayMetrics.heightPixels / 2 - } - - val formatListLinearLayout = view.findViewById(R.id.format_list_linear_layout) - val shimmers = view.findViewById(R.id.format_list_shimmer) - - videoFormatList = view.findViewById(R.id.video_linear_layout) - audioFormatList = view.findViewById(R.id.audio_linear_layout) - videoTitle = view.findViewById(R.id.video_title) - audioTitle = view.findViewById(R.id.audio_title) - okBtn = view.findViewById(R.id.format_ok) - - shimmers.visibility = View.GONE - isMissingFormats = formats.isEmpty() && items.any { it!!.allFormats.isEmpty() } - - if (items.size > 1){ - if (!isMissingFormats){ - chosenFormats = formats.mapTo(mutableListOf()) {it.copy()} - chosenFormats = when(items.first()?.type){ - Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) } - else -> chosenFormats - } - chosenFormats.forEach { - it.filesize = items.map { itm -> itm!!.allFormats }.flatten().filter { f -> f.format_id == it.format_id }.sumOf { itt -> itt.filesize } - } - }else{ - chosenFormats = formats - } - addFormatsToView() - }else{ - chosenFormats = formats - if(!isMissingFormats){ - if(items.first()?.type == Type.audio){ - chosenFormats = chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) } - } - } - addFormatsToView() - } - - refreshBtn = view.findViewById(R.id.format_refresh) - filterBtn.isVisible = chosenFormats.isNotEmpty() || items.all { it!!.url.isYoutubeURL() } - if (!isMissingFormats || items.isEmpty() || items.first()?.url?.isEmpty() == true || !canUpdate) { - refreshBtn.visibility = View.GONE - } - - refreshBtn.setOnClickListener { - lifecycleScope.launch { - val distinctItems = items.distinctBy { it!!.url } + lifecycleScope.launch { + val items = formatViewModel.selectedItems.value.toMutableList() + val distinctItems = items.distinctBy { it.url } - val itemsThatHaveFormats = distinctItems.filter { it!!.allFormats.isNotEmpty() } - val itemsWithMissingFormats = distinctItems.filter { it!!.allFormats.isEmpty() }.ifEmpty { distinctItems } + val itemsThatHaveFormats = distinctItems.filter { it.allFormats.isNotEmpty() } + val itemsWithMissingFormats = distinctItems.filter { it.allFormats.isEmpty() }.ifEmpty { distinctItems } - if (itemsWithMissingFormats.size > 10){ - continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_LONG) - continueInBackgroundSnackBar.setAction(R.string.ok) { - _multipleFormatsListener!!.onContinueOnBackground() - this@FormatSelectionBottomSheetDialog.dismiss() - } - continueInBackgroundSnackBar.show() - } + if (itemsWithMissingFormats.size > 10){ + continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_LONG) + continueInBackgroundSnackBar.setAction(R.string.ok) { + _multipleFormatsListener!!.onContinueOnBackground() + this@FormatSelectionBottomSheetDialog.dismiss() + } + continueInBackgroundSnackBar.show() + } - chosenFormats = emptyList() - refreshBtn.isEnabled = false - refreshBtn.isVisible = true - okBtn.isVisible = false - okBtn.isEnabled = false - filterBtn.isEnabled = false - formatListLinearLayout.visibility = View.GONE - shimmers.visibility = View.VISIBLE - shimmers.startShimmer() - updateFormatsJob = launch(Dispatchers.IO) { - try{ - //simple download - if (items.size == 1) { - kotlin.runCatching { - val res = resultViewModel.getFormats(items.first()!!.url, currentFormatSource) - if (!isActive) return@launch - res.filter { it.format_note != "storyboard" } - chosenFormats = if (items.first()?.type == Type.audio) { - res.filter { it.format_note.contains("audio", ignoreCase = true) } - } else { - res - } - if (chosenFormats.isEmpty()) throw Exception() + refreshBtn.isEnabled = false + refreshBtn.isVisible = true + okBtn.isVisible = false + okBtn.isEnabled = false + filterBtn.isEnabled = false + recyclerView.isVisible = false + shimmers.isVisible = true + shimmers.startShimmer() - formats.clear() - formats.addAll(res) + updateFormatsJob = launch(Dispatchers.IO) { + try{ + //simple download + if (items.size == 1) { + kotlin.runCatching { + val res = resultViewModel.getFormats(items.first().url, currentFormatSource) + if (!isActive) return@launch + res.filter { it.format_note != "storyboard" } + val chosenFormats = if (items.first().type == Type.audio) { + res.filter { it.format_note.contains("audio", ignoreCase = true) } + } else { + res + } + if (chosenFormats.isEmpty()) throw Exception() + items.first().allFormats.clear() + items.first().allFormats.addAll(chosenFormats) - withContext(Dispatchers.Main){ - listener.onFormatsUpdated(res) - } - }.onFailure { err -> - withContext(Dispatchers.Main){ - UiUtil.handleNoResults(requireActivity(), err.message.toString(), null, false, continued = {}, closed = {}, cookieFetch = {}) - } - } + withContext(Dispatchers.Main){ + formatViewModel.setItem(items.first()) + listener.onFormatsUpdated(res) + } + }.onFailure { err -> + withContext(Dispatchers.Main){ + UiUtil.handleNoResults(requireActivity(), err.message.toString(), null, false, continued = {}, closed = {}, cookieFetch = {}) + } + } - //list format filtering - }else{ - formats.clear() - var progressInt = 0 - val formatCollection = itemsThatHaveFormats.map { it!!.allFormats }.toMutableList() + //list format filtering + }else{ + var progressInt = 0 + val formatCollection = itemsThatHaveFormats.map { it.allFormats }.toMutableList() - var progress = "0/${itemsWithMissingFormats.size}" - withContext(Dispatchers.Main) { - refreshBtn.text = progress - } + var progress = "0/${itemsWithMissingFormats.size}" + withContext(Dispatchers.Main) { + refreshBtn.text = progress + } - val res = resultViewModel.getFormatsMultiple(itemsWithMissingFormats.map { it!!.url }, currentFormatSource) { - if (!isActive) return@getFormatsMultiple + val res = resultViewModel.getFormatsMultiple(itemsWithMissingFormats.map { it.url }, currentFormatSource) { + if (!isActive) return@getFormatsMultiple - if (it.unavailable) { - lifecycleScope.launch { - multipleFormatsListener.onItemUnavailable(it.url) - items.removeAt(items.indexOfFirst { item -> item!!.url == it.url }) - withContext(Dispatchers.Main) { - Snackbar.make(view, it.unavailableMessage, Snackbar.LENGTH_SHORT).show() - } - } - }else{ - multipleFormatsListener.onFormatUpdated(it.url, it.formats) - items.filter { item -> item!!.url == it.url }.forEach { d -> - d?.allFormats?.clear() - d?.allFormats?.addAll(it.formats) - } - progressInt++ - lifecycleScope.launch(Dispatchers.Main) { - progress = "${progressInt}/${itemsWithMissingFormats.size}" - refreshBtn.text = progress - } - } + if (it.unavailable) { + lifecycleScope.launch { + multipleFormatsListener.onItemUnavailable(it.url) + items.removeAt(items.indexOfFirst { item -> item.url == it.url }) + withContext(Dispatchers.Main) { + Snackbar.make(view, it.unavailableMessage, Snackbar.LENGTH_SHORT).show() + } + } + }else{ + multipleFormatsListener.onFormatUpdated(it.url, it.formats) + items.filter { item -> item.url == it.url }.forEach { d -> + d.allFormats.clear() + d.allFormats.addAll(it.formats) + } + progressInt++ + lifecycleScope.launch(Dispatchers.Main) { + progress = "${progressInt}/${itemsWithMissingFormats.size}" + refreshBtn.text = progress + } + } + } - } + formatViewModel.setItems(items) + } - formatCollection.addAll(res) - - if (!isActive) return@launch - - val flatFormatCollection = formatCollection.flatten() - val commonFormats = - flatFormatCollection.groupingBy { it.format_id }.eachCount() - .filter { it.value == distinctItems.size } - .mapValues { flatFormatCollection.first { f -> f.format_id == it.key } } - .map { it.value } - formats.addAll(commonFormats) - - chosenFormats = commonFormats.filter { it.filesize != 0L } - .mapTo(mutableListOf()) { it.copy() } - chosenFormats = when (items.first()?.type) { - Type.audio -> chosenFormats.filter { - it.vcodec.isBlank() || it.vcodec == "none" - } - - else -> chosenFormats - } - if (chosenFormats.isEmpty()) throw Exception() - chosenFormats.forEach { - it.filesize = - flatFormatCollection.filter { f -> f.format_id == it.format_id } - .sumOf { itt -> itt.filesize } - } - } - isMissingFormats = formats.isEmpty() - withContext(Dispatchers.Main){ - shimmers.visibility = View.GONE - shimmers.stopShimmer() - addFormatsToView() - refreshBtn.isVisible = isMissingFormats - refreshBtn.isEnabled = isMissingFormats - filterBtn.isEnabled = true - okBtn.isEnabled = true - formatListLinearLayout.visibility = View.VISIBLE - } - }catch (e: Exception){ - withContext(Dispatchers.Main) { - refreshBtn.isEnabled = true - filterBtn.isEnabled = true - okBtn.isEnabled = true - refreshBtn.text = getString(R.string.update) - formatListLinearLayout.visibility = View.VISIBLE - shimmers.visibility = View.GONE - shimmers.stopShimmer() - - e.printStackTrace() - Toast.makeText(context, getString(R.string.error_updating_formats), Toast.LENGTH_SHORT).show() - } - } - } - updateFormatsJob?.start() - } + withContext(Dispatchers.Main){ + filterBtn.isEnabled = true + okBtn.isEnabled = true + } + }catch (e: Exception){ + withContext(Dispatchers.Main) { + refreshBtn.isEnabled = true + filterBtn.isEnabled = true + okBtn.isEnabled = true + refreshBtn.text = getString(R.string.update) + recyclerView.isVisible = true + shimmers.visibility = View.GONE + shimmers.stopShimmer() + e.printStackTrace() + Toast.makeText(context, getString(R.string.error_updating_formats), Toast.LENGTH_SHORT).show() + } + } + } + updateFormatsJob?.start() + } } okBtn.setOnClickListener { @@ -364,6 +314,7 @@ class FormatSelectionBottomSheetDialog( filterSheet.setContentView(R.layout.format_category_sheet) //format filter + val isMissingFormats = formatViewModel.isMissingFormats.value filterSheet.findViewById(R.id.format_filter_linear)?.isVisible = !isMissingFormats if (!isMissingFormats) { val all = filterSheet.findViewById(R.id.all) @@ -373,7 +324,7 @@ class FormatSelectionBottomSheetDialog( val filterOptions = listOf(all!!, suggested!!,smallest!!, generic!!) filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } - when(filterBy) { + when(formatViewModel.filterBy.value) { FormatCategory.ALL -> all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) FormatCategory.SUGGESTED -> { suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) @@ -388,37 +339,34 @@ class FormatSelectionBottomSheetDialog( all.setOnClickListener { filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } - filterBy = FormatCategory.ALL all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) - addFormatsToView() filterSheet.dismiss() + formatViewModel.filterBy.value = FormatCategory.ALL } suggested.setOnClickListener { filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } - filterBy = FormatCategory.SUGGESTED suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) - addFormatsToView() filterSheet.dismiss() + formatViewModel.filterBy.value = FormatCategory.SUGGESTED } smallest.setOnClickListener { filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } - filterBy = FormatCategory.SMALLEST smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) - addFormatsToView() filterSheet.dismiss() + formatViewModel.filterBy.value = FormatCategory.SMALLEST } generic.setOnClickListener { filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } - filterBy = FormatCategory.GENERIC generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) - addFormatsToView() filterSheet.dismiss() + formatViewModel.filterBy.value = FormatCategory.GENERIC } } //format source val formatSourceLinear = filterSheet.findViewById(R.id.format_source_linear)!! - val canSwitch = items.all { it!!.url.isYoutubeURL() } + val items = formatViewModel.selectedItems.value + val canSwitch = items.all { it.url.isYoutubeURL() } formatSourceLinear.isVisible = canSwitch if (canSwitch) { val formatSourceOptions = mutableListOf() @@ -435,6 +383,7 @@ class FormatSelectionBottomSheetDialog( txt.tag = tag txt.setOnClickListener { currentFormatSource = it.tag.toString() + formatViewModel.filterBy.value = FormatCategory.ALL refreshBtn.performClick() filterSheet.dismiss() } @@ -447,33 +396,6 @@ class FormatSelectionBottomSheetDialog( } - //format layout - val listLayout = filterSheet.findViewById(R.id.layout_list)!! - val gridLayout = filterSheet.findViewById(R.id.layout_grid)!! - - if (usingGrid) { - listLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty, 0,0,0) - gridLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) - }else{ - gridLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty, 0,0,0) - listLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) - } - - listLayout.setOnClickListener { - usingGrid = false - gridLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty, 0,0,0) - listLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) - addFormatsToView() - filterSheet.dismiss() - } - gridLayout.setOnClickListener { - usingGrid = true - listLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty, 0,0,0) - gridLayout.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) - addFormatsToView() - filterSheet.dismiss() - } - val displayMetrics = DisplayMetrics() requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) filterSheet.behavior.peekHeight = displayMetrics.heightPixels @@ -482,240 +404,18 @@ class FormatSelectionBottomSheetDialog( } private fun returnFormats(){ - if (items.size == 1){ + if (_listener != null){ //simple video format selection - listener.onFormatClick(FormatTuple(selectedVideo, selectedAudios.ifEmpty { listOf(downloadViewModel.getFormat(chosenFormats, Type.audio)) })) + listener.onFormatClick(FormatTuple(adapter.selectedVideoFormat, adapter.selectedAudioFormats.ifEmpty { + listOf(downloadViewModel.getFormat(formats.filter { it.label == null }.map { it.format!! }, Type.audio)) + })) }else{ - //playlist format selection - val formatsToReturn = mutableListOf() - items.forEach { - formatsToReturn.add( - MultipleItemFormatTuple( - it!!.url, - FormatTuple( - it.allFormats.firstOrNull { f -> f.format_id == selectedVideo?.format_id }, - selectedAudios.map { sa -> - it.allFormats.first { a -> a.format_id == sa.format_id } - }.ifEmpty { null } - ) - ) - ) - } - multipleFormatsListener.onFormatClick(formatsToReturn) + val res = formatViewModel.getFormatsForItemsBasedOnFormat(adapter.selectedVideoFormat!!, adapter.selectedAudioFormats) + multipleFormatsListener.onFormatClick(res) } } - private fun addFormatsToView(){ - //sort - var finalFormats: List = when(sortBy){ - FormatSorting.container -> chosenFormats.groupBy { it.container }.flatMap { it.value } - FormatSorting.id -> chosenFormats.sortedBy { it.format_id } - FormatSorting.codec -> { - val codecOrder = resources.getStringArray(R.array.video_codec_values).toMutableList() - codecOrder.removeAt(0) - chosenFormats.groupBy { format -> codecOrder.indexOfFirst { format.vcodec.matches("^(${it})(.+)?$".toRegex()) } } - - .flatMap { - it.value.sortedByDescending { l -> l.filesize } - } - } - FormatSorting.filesize -> chosenFormats - } - - val formatSorter = FormatUtil(requireContext()) - - //filter category - when(filterBy){ - FormatCategory.ALL -> {} - FormatCategory.SUGGESTED -> { - finalFormats = if (items.first()?.type == Type.audio){ - formatSorter.sortAudioFormats(finalFormats) - }else{ - val audioFormats = finalFormats.filter { it.vcodec.isBlank() || it.vcodec == "none" } - val videoFormats = finalFormats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" } - - formatSorter.sortVideoFormats(videoFormats) + formatSorter.sortAudioFormats(audioFormats) - } - } - FormatCategory.SMALLEST -> { - val tmpFormats = finalFormats - .asSequence() - .map { it.copy() } - .filter { it.filesize > 0 } - .onEach { - var tmp = it.format_note - //formats that end like 1080P - if (tmp.endsWith("060")){ - tmp = tmp.removeSuffix("60") - } - tmp = tmp.removeSuffix("p") - //formats that are written like 1920X1080 - val split = tmp.split("x") - if (split.size > 1){ - tmp = split[1] - } - it.format_note = tmp - } - .groupBy { it.format_note } - .map { it.value.minBy { it2 -> it2.filesize } }.toList() - finalFormats = finalFormats.filter { tmpFormats.map { it2 -> it2.format_id }.contains(it.format_id) } - } - FormatCategory.GENERIC -> { - finalFormats = listOf() - } - } - - - - val canMultiSelectAudio = items.first()?.type == Type.video && finalFormats.find { it.vcodec.isBlank() || it.vcodec == "none" } != null - - if (!canMultiSelectAudio) { - audioFormatList.visibility = View.GONE - videoTitle.visibility = View.GONE - audioTitle.visibility = View.GONE - okBtn.visibility = View.GONE - }else{ - if (finalFormats.count { it.vcodec.isBlank() || it.vcodec == "none" } == 0){ - audioFormatList.visibility = View.GONE - audioTitle.visibility = View.GONE - videoTitle.visibility = View.GONE - okBtn.visibility = View.GONE - }else{ - audioFormatList.visibility = View.VISIBLE - audioTitle.visibility = View.VISIBLE - videoTitle.visibility = View.VISIBLE - okBtn.visibility = View.VISIBLE - } - } - - - videoFormatList.removeAllViews() - audioFormatList.removeAllViews() - - if (usingGrid) { - videoFormatList.columnCount = 2 - audioFormatList.columnCount = 2 - }else{ - videoFormatList.columnCount = 1 - audioFormatList.columnCount = 1 - } - - if (finalFormats.isEmpty()){ - finalFormats = if (items.first()?.type == Type.audio){ - genericAudioFormats - }else{ - genericVideoFormats - } - } - - for (i in 0.. finalFormats.lastIndex){ - val format = finalFormats[i] - lateinit var formatItem: View - - if (usingGrid){ - formatItem = LayoutInflater.from(context).inflate(R.layout.format_item_grid, null) - formatItem.layoutParams = GridLayout.LayoutParams( - GridLayout.spec(GridLayout.UNDEFINED, 1f), - GridLayout.spec(GridLayout.UNDEFINED, 1f)).apply { - width = 0 - } - }else{ - formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null) - formatItem.layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.MATCH_PARENT, - 1.0f - ) - } - formatItem.tag = "${format.format_id}${format.format_note}" - UiUtil.populateFormatCard(requireContext(), formatItem as MaterialCardView, format, null) - if (selectedVideo == format) formatItem.isChecked = true - if (selectedAudios.any { it == format }) formatItem.isChecked = true - formatItem.setOnClickListener{ clickedformat -> - //if the context is behind a video or playlist, allow the ability to multiselect audio formats - if (canMultiSelectAudio){ - val clickedCard = (clickedformat as MaterialCardView) - if (format.vcodec.isNotBlank() && format.vcodec != "none") { - if (clickedCard.isChecked) { - returnFormats() - dismiss() - } - videoFormatList.forEach { (it as MaterialCardView).isChecked = false } - selectedVideo = format - clickedCard.isChecked = true - }else{ - if(selectedAudios.contains(format)) { - selectedAudios.remove(format) - } else { - selectedAudios.add(format) - } - } - audioFormatList.forEach { (it as MaterialCardView).isChecked = false } - audioFormatList.forEach { - (it as MaterialCardView).isChecked = selectedAudios.map { a -> "${a.format_id}${a.format_note}" }.contains(it.tag) - } - }else{ - if (items.size == 1){ - listener.onFormatClick(FormatTuple(format, null)) - }else{ - val formatsToReturn = mutableListOf() - val f = if (genericAudioFormats.contains(format) || genericVideoFormats.contains(format)) format else null - items.forEach { - formatsToReturn.add( - MultipleItemFormatTuple( - it!!.url, - FormatTuple( - f ?: it.allFormats.firstOrNull { af -> af.format_id == format.format_id }, - null - ) - ) - ) - } - multipleFormatsListener.onFormatClick(formatsToReturn) - } - dismiss() - } - } - formatItem.setOnLongClickListener { - UiUtil.showFormatDetails(format, requireActivity()) - true - } - - if (canMultiSelectAudio){ - if (format.vcodec.isNotBlank() && format.vcodec != "none") videoFormatList.addView(formatItem) - else audioFormatList.addView(formatItem) - }else{ - videoFormatList.addView(formatItem) - } - - - } - - if (items.first()?.type == Type.video){ - selectedVideo = null - run breaking@{ - videoFormatList.children.forEach { - val card = it as MaterialCardView - if (card.isChecked){ - selectedVideo = finalFormats.first { format -> "${format.format_id}${format.format_note}" == card.tag } - return@breaking - } - } - } - }else{ - selectedAudios = mutableListOf() - run breaking@{ - audioFormatList.children.forEach { - val card = it as MaterialCardView - if (card.isChecked){ - selectedAudios.add(finalFormats.first { format -> "${format.format_id}${format.format_note}" == card.tag }) - } - } - } - } - } - override fun onCancel(dialog: DialogInterface) { super.onCancel(dialog) cleanUp() @@ -733,6 +433,16 @@ class FormatSelectionBottomSheetDialog( parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("formatSheet")!!).commit() } } + + override fun onItemSelect(item: Format, audioFormats: List?) { + if (_listener != null) { + listener.onFormatClick(FormatTuple(item, audioFormats)) + }else{ + val formatsToReturn = formatViewModel.getFormatsForItemsBasedOnFormat(item) + multipleFormatsListener.onFormatClick(formatsToReturn) + } + dismiss() + } } interface OnFormatClickListener{ diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt index 8ffc696c..5d95a244 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt @@ -272,7 +272,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() { } } - findPreference("youtube_home_recommendations")?.apply { + findPreference("home_recommendations")?.apply { val s = getString(R.string.video_recommendations_summary) summary = if (value.isNullOrBlank()) { s @@ -288,13 +288,20 @@ class GeneralSettingsFragment : BaseSettingsFragment() { if (newValue == "yt_api") { findPreference("api_key")?.isVisible = true + }else if (newValue == "custom") { + findPreference("custom_home_recommendation_url")?.isVisible = true } true } } + findPreference("custom_home_recommendation_url")?.apply { + title = "[${getString(R.string.video_recommendations)}] ${getString(R.string.custom)}" + isVisible = preferences.getString("home_recommendations", "") == "custom" + } + findPreference("api_key")?.apply { - isVisible = preferences.getString("youtube_home_recommendations", "") == "yt_api" + isVisible = preferences.getString("home_recommendations", "") == "yt_api" val s = getString(R.string.api_key_summary) summary = if (text.isNullOrBlank()) { s diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/generateyoutubepotokens/GenerateYoutubePoTokensFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/generateyoutubepotokens/GenerateYoutubePoTokensFragment.kt index 3e86715c..cb06d20d 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/generateyoutubepotokens/GenerateYoutubePoTokensFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/generateyoutubepotokens/GenerateYoutubePoTokensFragment.kt @@ -1,29 +1,45 @@ package com.deniscerri.ytdl.ui.more.settings.advanced.generateyoutubepotokens import android.app.Activity +import android.content.Context.INPUT_METHOD_SERVICE import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.Window +import android.view.inputmethod.InputMethodManager +import android.widget.EditText import android.widget.TextView +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.work.WorkManager import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.database.models.CookieItem import com.deniscerri.ytdl.database.models.YoutubeGeneratePoTokenItem import com.deniscerri.ytdl.database.models.YoutubePoTokenItem +import com.deniscerri.ytdl.ui.more.WebViewActivity import com.deniscerri.ytdl.ui.more.settings.SettingsActivity import com.deniscerri.ytdl.ui.more.settings.advanced.generateyoutubepotokens.webview.PoTokenWebViewLoginActivity +import com.deniscerri.ytdl.util.Extensions.enableTextHighlight +import com.deniscerri.ytdl.util.Extensions.isYoutubeURL import com.deniscerri.ytdl.util.UiUtil +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton +import com.google.android.material.card.MaterialCardView import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.snackbar.Snackbar import com.google.gson.Gson +import kotlinx.coroutines.launch class GenerateYoutubePoTokensFragment : Fragment() { private lateinit var settingsActivity: SettingsActivity @@ -31,6 +47,8 @@ class GenerateYoutubePoTokensFragment : Fragment() { private lateinit var configuration : MutableList private lateinit var workManager : WorkManager + private lateinit var webPoTokenResultLauncher : ActivityResultLauncher + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -119,7 +137,7 @@ class GenerateYoutubePoTokensFragment : Fragment() { .show() } - val webPoTokenResultLauncher = registerForActivityResult( + webPoTokenResultLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> if (result.resultCode == Activity.RESULT_OK) { @@ -137,13 +155,11 @@ class GenerateYoutubePoTokensFragment : Fragment() { configuration.add(conf) setValues(conf) preferences.edit().putString("youtube_generated_po_tokens", Gson().toJson(configuration).toString()).apply() - }else { - Snackbar.make(requireView(), R.string.network_error, Snackbar.LENGTH_SHORT).show() } } regenerate.setOnClickListener { - webPoTokenResultLauncher.launch(Intent(requireContext(), PoTokenWebViewLoginActivity::class.java)) + showBottomSheet() } switch.setOnClickListener { @@ -168,4 +184,44 @@ class GenerateYoutubePoTokensFragment : Fragment() { } } + + private fun showBottomSheet(){ + lifecycleScope.launch { + val layout = BottomSheetDialog(requireContext()) + layout.requestWindowFeature(Window.FEATURE_NO_TITLE) + layout.setContentView(R.layout.generate_po_token_url_bottom_sheet) + + val editText = layout.findViewById(R.id.url_edittext)!! + val text = preferences.getString("genenerate_youtube_po_token_preferred_url", "https://youtube.com/account") + editText.setText(text) + editText.setSelection(editText.text.length) + + val regenerateBtn = layout.findViewById(R.id.getPoTokenBtn)!! + + editText.doOnTextChanged { text, start, before, count -> + regenerateBtn.isEnabled = editText.text.toString().isYoutubeURL() + } + + regenerateBtn.setOnClickListener { + val intent = Intent(requireContext(), PoTokenWebViewLoginActivity::class.java) + intent.putExtra("url", editText.text.toString()) + webPoTokenResultLauncher.launch(intent) + } + + + val imm = requireActivity().getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + editText.postDelayed({ + editText.requestFocus() + imm.showSoftInput(editText, 0) + }, 300) + + layout.show() + layout.behavior.state = BottomSheetBehavior.STATE_EXPANDED + layout.window!!.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/generateyoutubepotokens/webview/PoTokenWebViewLoginActivity.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/generateyoutubepotokens/webview/PoTokenWebViewLoginActivity.kt index 2434f431..efc5ba11 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/generateyoutubepotokens/webview/PoTokenWebViewLoginActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/generateyoutubepotokens/webview/PoTokenWebViewLoginActivity.kt @@ -55,6 +55,8 @@ class PoTokenWebViewLoginActivity : BaseActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.webview_activity) + val url = intent.getStringExtra("url")!! + cookiesViewModel = ViewModelProvider(this)[CookieViewModel::class.java] lifecycleScope.launch { val appbar = findViewById(R.id.webview_appbarlayout) @@ -77,6 +79,7 @@ class PoTokenWebViewLoginActivity : BaseActivity() { cookieManager = CookieManager.getInstance() preferences = PreferenceManager.getDefaultSharedPreferences(this@PoTokenWebViewLoginActivity) + preferences.edit().putString("genenerate_youtube_po_token_preferred_url", url).apply() webViewClient = object : AccompanistWebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { @@ -113,13 +116,13 @@ class PoTokenWebViewLoginActivity : BaseActivity() { //update cookies withContext(Dispatchers.IO) { - val url = "Po Token Generated Cookies" - cookiesViewModel.getCookiesFromDB(url).getOrNull()?.let { + val cookieURL = "Po Token Generated Cookies" + cookiesViewModel.getCookiesFromDB(cookieURL).getOrNull()?.let { kotlin.runCatching { cookiesViewModel.insert( CookieItem( 0, - url, + cookieURL, it ) ) @@ -137,8 +140,6 @@ class PoTokenWebViewLoginActivity : BaseActivity() { } } - - webView.clearCache(true) // ensures that the WebView isn't doing anything when destroying it webView.loadUrl("about:blank") @@ -150,7 +151,7 @@ class PoTokenWebViewLoginActivity : BaseActivity() { } webViewCompose.apply { - setContent { WebViewView() } + setContent { WebViewView(url) } } } @@ -158,7 +159,7 @@ class PoTokenWebViewLoginActivity : BaseActivity() { @SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") @Composable - fun WebViewView() { + fun WebViewView(url: String) { val webViewChromeClient = remember { object : AccompanistWebChromeClient() { } @@ -166,7 +167,7 @@ class PoTokenWebViewLoginActivity : BaseActivity() { Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues -> WebView( - state = rememberWebViewState("https://youtube.com/account"), client = webViewClient, chromeClient = webViewChromeClient, + state = rememberWebViewState(url), client = webViewClient, chromeClient = webViewChromeClient, modifier = Modifier .padding(paddingValues) .fillMaxSize(), 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 08d56072..fed688b1 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -179,6 +179,7 @@ object UiUtil { if (chosenFormat.tbr.isNullOrBlank() || (chosenFormat.vcodec.isNotBlank() && chosenFormat.vcodec != "none")) { isVisible = false }else{ + isVisible = true text = chosenFormat.tbr } } diff --git a/app/src/main/res/layout/download_card.xml b/app/src/main/res/layout/download_card.xml index 9c3d8f43..2a8d0739 100644 --- a/app/src/main/res/layout/download_card.xml +++ b/app/src/main/res/layout/download_card.xml @@ -172,7 +172,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="5dp" android:background="@drawable/rounded_corner" - android:backgroundTint="?attr/colorSecondary" + android:backgroundTint="?attr/colorPrimaryInverse" android:clickable="false" android:ellipsize="end" android:gravity="center" diff --git a/app/src/main/res/layout/format_category_sheet.xml b/app/src/main/res/layout/format_category_sheet.xml index 6ccd9830..c08d4727 100644 --- a/app/src/main/res/layout/format_category_sheet.xml +++ b/app/src/main/res/layout/format_category_sheet.xml @@ -102,54 +102,4 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/format_select_bottom_sheet.xml b/app/src/main/res/layout/format_select_bottom_sheet.xml index 949d4791..fce00445 100644 --- a/app/src/main/res/layout/format_select_bottom_sheet.xml +++ b/app/src/main/res/layout/format_select_bottom_sheet.xml @@ -2,6 +2,7 @@ @@ -103,81 +104,18 @@ - - - - - - -