more fixes

This commit is contained in:
deniscerri 2025-03-29 22:35:39 +01:00
parent db55894fdc
commit 1426ceef0a
No known key found for this signature in database
GPG key ID: 95C43D517D830350
24 changed files with 735 additions and 679 deletions

View file

@ -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,
)

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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<DownloadItem>())
val selectedItemsSharedFlow = MutableSharedFlow<List<DownloadItem>>(replay = 1)
var formats : Flow<List<FormatRecyclerView>>
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<Format>
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<Format> = 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<FormatRecyclerView>()
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<DownloadItem>, 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<Format>? = null) : MutableList<MultipleItemFormatTuple> {
val formatsToReturn = mutableListOf<MultipleItemFormatTuple>()
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
}
}

View file

@ -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()

View file

@ -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

View file

@ -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<Format?, FormatAdapter.ViewHolder>(AsyncDifferConfig.Builder(
class FormatAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<FormatRecyclerView?, FormatAdapter.ViewHolder>(AsyncDifferConfig.Builder(
DIFF_CALLBACK
).build()) {
private val onItemClickListener: OnItemClickListener
private val activity: Activity
private var selectedVideoFormat: Format?
private val selectedAudioFormats: MutableList<Format>
private val usingGrid: Boolean
var selectedVideoFormat: Format? = null
val selectedAudioFormats: MutableList<Format> = mutableListOf()
private var canMultiSelectAudio: Boolean = false
private var formats: MutableList<FormatRecyclerView?> = 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<FormatRecyclerView?>?) {
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<Format>?)
}
companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<Format> = object : DiffUtil.ItemCallback<Format>() {
override fun areItemsTheSame(oldItem: Format, newItem: Format): Boolean {
return oldItem.format_id == newItem.format_id
private val DIFF_CALLBACK: DiffUtil.ItemCallback<FormatRecyclerView> = object : DiffUtil.ItemCallback<FormatRecyclerView>() {
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
}
}
}

View file

@ -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<Format>
@ -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")
}
}

View file

@ -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<View>
@ -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")
}
}

View file

@ -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<String>
@ -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")
}
}

View file

@ -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<DownloadItem?>? = 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<Format>
private lateinit var genericVideoFormats : List<Format>
private var formats: List<FormatRecyclerView> = listOf()
private var canMultiSelectAudio: Boolean = false
private lateinit var behavior: BottomSheetBehavior<View>
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<Format>
private var selectedVideo : Format? = null
private lateinit var selectedAudios : MutableList<Format>
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<DownloadItem?>
private lateinit var formats: MutableList<Format>
private lateinit var listener: OnFormatClickListener
private lateinit var multipleFormatsListener: OnMultipleFormatClickListener
private var currentFormatSource : String? = null
private lateinit var genericAudioFormats : List<Format>
private lateinit var genericVideoFormats : List<Format>
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<TextView>(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<ShimmerFrameLayout>(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<LinearLayout>(R.id.format_list_linear_layout)
val shimmers = view.findViewById<ShimmerFrameLayout>(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<LinearLayout>(R.id.format_filter_linear)?.isVisible = !isMissingFormats
if (!isMissingFormats) {
val all = filterSheet.findViewById<TextView>(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<LinearLayout>(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<TextView>()
@ -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<TextView>(R.id.layout_list)!!
val gridLayout = filterSheet.findViewById<TextView>(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<MultipleItemFormatTuple>()
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<Format> = 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<MultipleItemFormatTuple>()
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<Format>?) {
if (_listener != null) {
listener.onFormatClick(FormatTuple(item, audioFormats))
}else{
val formatsToReturn = formatViewModel.getFormatsForItemsBasedOnFormat(item)
multipleFormatsListener.onFormatClick(formatsToReturn)
}
dismiss()
}
}
interface OnFormatClickListener{

View file

@ -272,7 +272,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
}
}
findPreference<ListPreference>("youtube_home_recommendations")?.apply {
findPreference<ListPreference>("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<EditTextPreference>("api_key")?.isVisible = true
}else if (newValue == "custom") {
findPreference<EditTextPreference>("custom_home_recommendation_url")?.isVisible = true
}
true
}
}
findPreference<EditTextPreference>("custom_home_recommendation_url")?.apply {
title = "[${getString(R.string.video_recommendations)}] ${getString(R.string.custom)}"
isVisible = preferences.getString("home_recommendations", "") == "custom"
}
findPreference<EditTextPreference>("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

View file

@ -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<YoutubeGeneratePoTokenItem>
private lateinit var workManager : WorkManager
private lateinit var webPoTokenResultLauncher : ActivityResultLauncher<Intent>
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<EditText>(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<MaterialButton>(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
)
}
}
}

View file

@ -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<AppBarLayout>(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(),

View file

@ -179,6 +179,7 @@ object UiUtil {
if (chosenFormat.tbr.isNullOrBlank() || (chosenFormat.vcodec.isNotBlank() && chosenFormat.vcodec != "none")) {
isVisible = false
}else{
isVisible = true
text = chosenFormat.tbr
}
}

View file

@ -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"

View file

@ -102,54 +102,4 @@
</LinearLayout>
<LinearLayout
android:id="@+id/format_layout_linear"
android:layout_width="match_parent"
android:visibility="gone"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="10dp"
android:layout_marginTop="20dp"
android:text="@string/layout"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/layout_list"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:drawablePadding="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:text="@string/list"
app:drawableStartCompat="@drawable/ic_check" />
<TextView
android:id="@+id/layout_grid"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:drawablePadding="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:text="@string/grid"
app:drawableStartCompat="@drawable/empty" />
</LinearLayout>
</LinearLayout>

View file

@ -2,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_height="wrap_content">
@ -103,81 +104,18 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.core.widget.NestedScrollView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/format_list_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/video_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:clickable="false"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:gravity="start"
app:cornerRadius="0dp"
android:textStyle="bold"
android:textSize="16sp"
android:paddingVertical="20dp"
android:text="@string/video" />
<GridLayout
android:id="@+id/video_linear_layout"
android:layout_width="match_parent"
android:columnCount="1"
android:alignmentMode="alignBounds"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/audio_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:textStyle="bold"
android:textSize="16sp"
android:clickable="false"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:gravity="start"
app:cornerRadius="0dp"
android:paddingVertical="20dp"
android:text="@string/audio" />
<GridLayout
android:id="@+id/audio_linear_layout"
android:layout_width="match_parent"
android:columnCount="1"
android:alignmentMode="alignBounds"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
android:layout_height="wrap_content"
tools:listitem="@layout/format_item" />
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/format_list_shimmer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:orientation="vertical"
android:paddingHorizontal="10dp"
android:paddingTop="10dp">

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Button android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:clickable="false"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:gravity="start"
app:cornerRadius="0dp"
android:textStyle="bold"
android:textSize="16sp"
android:paddingVertical="20dp"
android:text="@string/video"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" />

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:scrollbars="none"
android:orientation="vertical"
android:layout_height="wrap_content"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="20dp"
android:hint="@string/url"
app:layout_constraintTop_toBottomOf="@+id/scr">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/url_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:inputType="textMultiLine"
android:maxLines="2000" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/getPoTokenBtn"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:layout_margin="20dp"
android:text="@string/regenerate"
app:icon="@drawable/baseline_stars_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -24,7 +24,7 @@
<item
android:id="@+id/redownload"
android:icon="@drawable/baseline_share_24"
android:icon="@drawable/baseline_download_24"
app:showAsAction="ifRoom"
android:title="@string/redownload" />

View file

@ -1151,6 +1151,7 @@
<item>@string/ytdlp_recommnedations</item>
<item>@string/ytdlp_liked</item>
<item>@string/ytdlp_watch_history</item>
<item>@string/custom</item>
</array>
<array name="video_recommnedations_values">
@ -1161,6 +1162,7 @@
<item>yt_dlp_recommendations</item>
<item>yt_dlp_liked</item>
<item>yt_dlp_watch_history</item>
<item>custom</item>
</array>
<!--below is for preferred audio language, not related to app's language-->

View file

@ -204,7 +204,7 @@
<string name="subtitle_languages">Subtitle languages</string>
<string name="format_source">Formats Source</string>
<string name="video_recommendations">Video Recommendations</string>
<string name="video_recommendations_summary">Get recommended YouTube videos on the home screen</string>
<string name="video_recommendations_summary">Get recommended videos on the home screen</string>
<string name="preferred_search_engine">Preferred Search Engine</string>
<string name="preferred_search_engine_summary">The search engine to use for in-app searches</string>
<string name="format_filtering_hint">All items must be of the same type to use this option</string>
@ -475,4 +475,5 @@
<string name="generate_potokens">Generate PO Tokens</string>
<string name="regenerate">Re-generate</string>
<string name="generate_potokens_warning">* By enabling this, you need to disable cookies</string>
<string name="custom">Custom</string>
</resources>

View file

@ -71,13 +71,18 @@
<PreferenceCategory android:title="YouTube">
<ListPreference
android:icon="@drawable/baseline_recommend_24"
android:key="youtube_home_recommendations"
android:key="home_recommendations"
android:defaultValue="newpipe"
android:entries="@array/video_recommendations"
android:entryValues="@array/video_recommnedations_values"
app:summary="@string/video_recommendations_summary"
app:title="@string/video_recommendations" />
<EditTextPreference
android:icon="@drawable/baseline_recommend_24"
app:key="custom_home_recommendation_url"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
android:icon="@drawable/ic_key"
app:key="api_key"
@ -120,7 +125,7 @@
android:entryValues="@array/countries_values"
app:icon="@drawable/ic_language"
app:useSimpleSummaryProvider="true"
app:dependency="youtube_home_recommendations"
app:dependency="home_recommendations"
app:key="locale"
app:title="@string/preferred_locale" />