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(){ suspend fun getHomeRecommendations(){
deleteAll() deleteAll()
val category = sharedPreferences.getString("youtube_home_recommendations", "") val category = sharedPreferences.getString("home_recommendations", "")
val items = when(category) { val items = when(category) {
"newpipe" -> newPipeUtil.getTrending() "newpipe" -> newPipeUtil.getTrending()
"yt_api" -> youtubeApiUtil.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_recommendations" -> ytdlpUtil.getYoutubeRecommendations()
"yt_dlp_liked" -> ytdlpUtil.getYoutubeLikedVideos() "yt_dlp_liked" -> ytdlpUtil.getYoutubeLikedVideos()
"yt_dlp_watch_history" -> ytdlpUtil.getYoutubeWatchHistory() "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() else -> arrayListOf()
} }

View file

@ -1099,8 +1099,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
if (it.type == Type.video) { if (it.type == Type.video) {
it.videoPreferences.audioFormatIDs.clear()
ft.audioFormats?.map { a -> a.format_id }?.let { list -> ft.audioFormats?.map { a -> a.format_id }?.let { list ->
it.videoPreferences.audioFormatIDs.clear()
it.videoPreferences.audioFormatIDs.addAll(list) it.videoPreferences.audioFormatIDs.addAll(list)
} }
} }
@ -1342,8 +1342,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
cancelAllDownloadsImpl() cancelAllDownloadsImpl()
} }
private fun cancelAllDownloadsImpl() { private suspend fun cancelAllDownloadsImpl() {
WorkManager.getInstance(application).cancelAllWorkByTag("download") WorkManager.getInstance(application).cancelAllWorkByTag("download")
val activeDownloadsList = withContext(Dispatchers.IO){
getActiveDownloads()
}
activeDownloadsList.forEach {
cancelDownloadOnly(it.id)
}
cancelActiveQueued() 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){ 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 { kotlin.runCatching {
uiState.update {it.copy(processing = true)} uiState.update {it.copy(processing = true)}
repository.getHomeRecommendations() repository.getHomeRecommendations()

View file

@ -102,7 +102,7 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
} else { } else {
item.format.acodec.uppercase() item.format.acodec.uppercase()
} }
if (codecText == "" || codecText == "none"){ if (codecText == "" || codecText == "none" || codecText == "DEFAULT"){
codec.visibility = View.GONE codec.visibility = View.GONE
}else{ }else{
codec.visibility = View.VISIBLE codec.visibility = View.VISIBLE

View file

@ -4,6 +4,7 @@ import android.app.Activity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@ -12,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.CookieItem import com.deniscerri.ytdl.database.models.CookieItem
import com.deniscerri.ytdl.database.models.Format 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
import com.deniscerri.ytdl.databinding.FormatItemBinding import com.deniscerri.ytdl.databinding.FormatItemBinding
import com.deniscerri.ytdl.ui.adapter.HistoryPaginatedAdapter.ViewHolder 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.deniscerri.ytdl.util.UiUtil
import com.google.android.material.card.MaterialCardView 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 DIFF_CALLBACK
).build()) { ).build()) {
private val onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener
private val activity: Activity private val activity: Activity
private var selectedVideoFormat: Format? var selectedVideoFormat: Format? = null
private val selectedAudioFormats: MutableList<Format> val selectedAudioFormats: MutableList<Format> = mutableListOf()
private val usingGrid: Boolean private var canMultiSelectAudio: Boolean = false
private var formats: MutableList<FormatRecyclerView?> = mutableListOf()
init { init {
this.onItemClickListener = onItemClickListener this.onItemClickListener = onItemClickListener
this.activity = activity this.activity = activity
this.selectedVideoFormat = null
this.usingGrid = false
this.selectedAudioFormats = mutableListOf()
} }
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { 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 { override fun submitList(list: MutableList<FormatRecyclerView?>?) {
item = itemView.findViewById(R.id.format_card_constraintLayout) 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return if (usingGrid){ return if (viewType == 0){
val cardView = LayoutInflater.from(parent.context) val button = LayoutInflater.from(parent.context)
.inflate(R.layout.format_item_grid, parent, false) .inflate(R.layout.format_type_label, parent, false)
ViewHolder( ViewHolder(
cardView, button,
onItemClickListener onItemClickListener
) )
}else{ }else{
@ -67,54 +84,65 @@ class FormatAdapter(onItemClickListener: OnItemClickListener, activity: Activity
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) ?: return val itm = getItem(position) ?: return
val card = holder.item val viewType = getItemViewType(position)
card.popup() 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) UiUtil.populateFormatCard(activity, card, item)
card.isChecked = selectedVideoFormat == item || selectedAudioFormats.any { it == item }
card.setOnClickListener { card.setOnClickListener {
when(downloadType) { if (!canMultiSelectAudio) {
DownloadViewModel.Type.audio -> { onItemClickListener.onItemSelect(item, null)
onItemClickListener.onItemClick(item) }else {
} if (item.isVideo()) {
DownloadViewModel.Type.video -> { if (card.isChecked) {
if(item.isAudio()) { onItemClickListener.onItemSelect(item, selectedAudioFormats)
if (card.isChecked) {
selectedAudioFormats.remove(item)
}else {
selectedAudioFormats.add(item)
}
}else { }else {
if (card.isChecked) { selectedVideoFormat = item
onItemClickListener.onItemClick(item) notifyDataSetChanged()
}else {
selectedVideoFormat = item
card.isChecked = true
}
} }
}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" return this.vcodec.isNotBlank() && this.vcodec != "none"
} }
interface OnItemClickListener { interface OnItemClickListener {
fun onItemClick(item: Format) fun onItemSelect(item: Format, audioFormats: List<Format>?)
} }
companion object { companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<Format> = object : DiffUtil.ItemCallback<Format>() { private val DIFF_CALLBACK: DiffUtil.ItemCallback<FormatRecyclerView> = object : DiffUtil.ItemCallback<FormatRecyclerView>() {
override fun areItemsTheSame(oldItem: Format, newItem: Format): Boolean { override fun areItemsTheSame(oldItem: FormatRecyclerView, newItem: FormatRecyclerView): Boolean {
return oldItem.format_id == newItem.format_id return oldItem.label == newItem.label && oldItem.format?.format_id == newItem.format?.format_id
} }
override fun areContentsTheSame(oldItem: Format, newItem: Format): Boolean { override fun areContentsTheSame(oldItem: FormatRecyclerView, newItem: FormatRecyclerView): Boolean {
return oldItem.format_id == newItem.format_id 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.models.ResultItem
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type 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.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.applyFilenameTemplateForCuts import com.deniscerri.ytdl.util.Extensions.applyFilenameTemplateForCuts
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
@ -54,6 +55,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var resultViewModel : ResultViewModel private lateinit var resultViewModel : ResultViewModel
private lateinit var formatViewModel : FormatViewModel
private lateinit var saveDir : TextInputLayout private lateinit var saveDir : TextInputLayout
private lateinit var freeSpace : TextView private lateinit var freeSpace : TextView
private lateinit var genericAudioFormats: MutableList<Format> private lateinit var genericAudioFormats: MutableList<Format>
@ -73,6 +75,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
activity = getActivity() activity = getActivity()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
formatViewModel = ViewModelProvider(requireActivity())[FormatViewModel::class.java]
genericAudioFormats = FormatUtil(requireContext()).getGenericAudioFormats(requireContext().resources) genericAudioFormats = FormatUtil(requireContext()).getGenericAudioFormats(requireContext().resources)
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
shownFields = preferences.getStringSet("modify_download_card", requireContext().getStringArray(R.array.modify_download_card_values).toSet())!!.toList() 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{ formatCard.setOnClickListener{
if (parentFragmentManager.findFragmentByTag("formatSheet") == null){ 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") 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.models.Format
import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel 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.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.receiver.ShareActivity import com.deniscerri.ytdl.receiver.ShareActivity
@ -70,6 +71,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var historyViewModel: HistoryViewModel private lateinit var historyViewModel: HistoryViewModel
private lateinit var commandTemplateViewModel: CommandTemplateViewModel private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var formatViewModel: FormatViewModel
private lateinit var listAdapter : ConfigureMultipleDownloadsAdapter private lateinit var listAdapter : ConfigureMultipleDownloadsAdapter
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
@ -97,6 +99,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java] historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java]
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java] resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
formatViewModel = ViewModelProvider(requireActivity())[FormatViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java] commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
@ -506,7 +509,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val items = withContext(Dispatchers.IO){ val items = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloads() downloadViewModel.getProcessingDownloads()
} }
val bottomSheet = FormatSelectionBottomSheetDialog(items, _multipleFormatsListener = formatListener) formatViewModel.setItems(items)
val bottomSheet = FormatSelectionBottomSheetDialog( _multipleFormatsListener = formatListener)
bottomSheet.show(parentFragmentManager, "formatSheet") 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.models.ResultItem
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type 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.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.applyFilenameTemplateForCuts import com.deniscerri.ytdl.util.Extensions.applyFilenameTemplateForCuts
import com.deniscerri.ytdl.util.FileUtil 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 fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var formatViewModel : FormatViewModel
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var preferences: SharedPreferences private lateinit var preferences: SharedPreferences
private lateinit var shownFields: List<String> private lateinit var shownFields: List<String>
@ -77,6 +79,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
activity = getActivity() activity = getActivity()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
formatViewModel = ViewModelProvider(requireActivity())[FormatViewModel::class.java]
val formatUtil = FormatUtil(requireContext()) val formatUtil = FormatUtil(requireContext())
genericVideoFormats = formatUtil.getGenericVideoFormats(requireContext().resources) genericVideoFormats = formatUtil.getGenericVideoFormats(requireContext().resources)
genericAudioFormats = formatUtil.getGenericAudioFormats(requireContext().resources) genericAudioFormats = formatUtil.getGenericAudioFormats(requireContext().resources)
@ -290,7 +293,8 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
} }
formatCard.setOnClickListener{ formatCard.setOnClickListener{
if (parentFragmentManager.findFragmentByTag("formatSheet") == null){ 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") bottomSheet.show(parentFragmentManager, "formatSheet")
} }
} }

View file

@ -20,13 +20,19 @@ import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.Format 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
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type 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.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.adapter.FormatAdapter 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.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.FormatUtil import com.deniscerri.ytdl.util.FormatUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
@ -39,17 +45,27 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.w3c.dom.Text
class FormatSelectionBottomSheetDialog( class FormatSelectionBottomSheetDialog(
private val _items: List<DownloadItem?>? = null,
private val _listener: OnFormatClickListener? = null, private val _listener: OnFormatClickListener? = null,
private val canUpdate: Boolean = true,
private val _multipleFormatsListener: OnMultipleFormatClickListener? = null 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 behavior: BottomSheetBehavior<View>
private lateinit var formatUtil: FormatUtil private lateinit var formatUtil: FormatUtil
@ -58,36 +74,17 @@ class FormatSelectionBottomSheetDialog(
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var videoFormatList : GridLayout
private lateinit var audioFormatList : GridLayout
private lateinit var okBtn : Button private lateinit var okBtn : Button
private lateinit var refreshBtn: 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 lateinit var filterBtn : Button
private var updateFormatsJob: Job? = null 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 listener: OnFormatClickListener
private lateinit var multipleFormatsListener: OnMultipleFormatClickListener private lateinit var multipleFormatsListener: OnMultipleFormatClickListener
private var currentFormatSource : String? = null 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 { enum class FormatSorting {
filesize, container, codec, id filesize, container, codec, id
} }
@ -98,9 +95,8 @@ class FormatSelectionBottomSheetDialog(
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
formatViewModel = ViewModelProvider(requireActivity())[FormatViewModel::class.java]
formatUtil = FormatUtil(requireContext()) formatUtil = FormatUtil(requireContext())
chosenFormats = listOf()
selectedAudios = mutableListOf()
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::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) view = LayoutInflater.from(context).inflate(R.layout.format_select_bottom_sheet, null)
dialog.setContentView(view) dialog.setContentView(view)
if (_items == null){ dialog.setOnShowListener {
this.dismiss() behavior = BottomSheetBehavior.from(view.parent as View)
return val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
behavior.peekHeight = displayMetrics.heightPixels / 2
} }
items = _items.toMutableList() view.findViewById<TextView>(R.id.bottom_sheet_title).setOnClickListener {
if (items.size == 1) { recyclerView.scrollTo(0,0)
formats = items.first()!!.allFormats }
}else{
val flatFormatCollection = items.map { it!!.allFormats }.flatten() genericAudioFormats = formatUtil.getGenericAudioFormats(resources)
formats = flatFormatCollection.groupingBy { it.format_id }.eachCount() genericVideoFormats = formatUtil.getGenericVideoFormats(resources)
.filter { it.value == items.size }
.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } } adapter =
.map { it.value }.toMutableList() 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 { _listener?.apply {
@ -137,211 +179,119 @@ class FormatSelectionBottomSheetDialog(
multipleFormatsListener = this 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 { refreshBtn.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
val distinctItems = items.distinctBy { it!!.url } val items = formatViewModel.selectedItems.value.toMutableList()
val distinctItems = items.distinctBy { it.url }
val itemsThatHaveFormats = distinctItems.filter { it!!.allFormats.isNotEmpty() } val itemsThatHaveFormats = distinctItems.filter { it.allFormats.isNotEmpty() }
val itemsWithMissingFormats = distinctItems.filter { it!!.allFormats.isEmpty() }.ifEmpty { distinctItems } val itemsWithMissingFormats = distinctItems.filter { it.allFormats.isEmpty() }.ifEmpty { distinctItems }
if (itemsWithMissingFormats.size > 10){ if (itemsWithMissingFormats.size > 10){
continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_LONG) continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_LONG)
continueInBackgroundSnackBar.setAction(R.string.ok) { continueInBackgroundSnackBar.setAction(R.string.ok) {
_multipleFormatsListener!!.onContinueOnBackground() _multipleFormatsListener!!.onContinueOnBackground()
this@FormatSelectionBottomSheetDialog.dismiss() this@FormatSelectionBottomSheetDialog.dismiss()
} }
continueInBackgroundSnackBar.show() continueInBackgroundSnackBar.show()
} }
chosenFormats = emptyList() refreshBtn.isEnabled = false
refreshBtn.isEnabled = false refreshBtn.isVisible = true
refreshBtn.isVisible = true okBtn.isVisible = false
okBtn.isVisible = false okBtn.isEnabled = false
okBtn.isEnabled = false filterBtn.isEnabled = false
filterBtn.isEnabled = false recyclerView.isVisible = false
formatListLinearLayout.visibility = View.GONE shimmers.isVisible = true
shimmers.visibility = View.VISIBLE shimmers.startShimmer()
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()
formats.clear() updateFormatsJob = launch(Dispatchers.IO) {
formats.addAll(res) 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){ withContext(Dispatchers.Main){
listener.onFormatsUpdated(res) formatViewModel.setItem(items.first())
} listener.onFormatsUpdated(res)
}.onFailure { err -> }
withContext(Dispatchers.Main){ }.onFailure { err ->
UiUtil.handleNoResults(requireActivity(), err.message.toString(), null, false, continued = {}, closed = {}, cookieFetch = {}) withContext(Dispatchers.Main){
} UiUtil.handleNoResults(requireActivity(), err.message.toString(), null, false, continued = {}, closed = {}, cookieFetch = {})
} }
}
//list format filtering //list format filtering
}else{ }else{
formats.clear() var progressInt = 0
var progressInt = 0 val formatCollection = itemsThatHaveFormats.map { it.allFormats }.toMutableList()
val formatCollection = itemsThatHaveFormats.map { it!!.allFormats }.toMutableList()
var progress = "0/${itemsWithMissingFormats.size}" var progress = "0/${itemsWithMissingFormats.size}"
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
refreshBtn.text = progress refreshBtn.text = progress
} }
val res = resultViewModel.getFormatsMultiple(itemsWithMissingFormats.map { it!!.url }, currentFormatSource) { val res = resultViewModel.getFormatsMultiple(itemsWithMissingFormats.map { it.url }, currentFormatSource) {
if (!isActive) return@getFormatsMultiple if (!isActive) return@getFormatsMultiple
if (it.unavailable) { if (it.unavailable) {
lifecycleScope.launch { lifecycleScope.launch {
multipleFormatsListener.onItemUnavailable(it.url) multipleFormatsListener.onItemUnavailable(it.url)
items.removeAt(items.indexOfFirst { item -> item!!.url == it.url }) items.removeAt(items.indexOfFirst { item -> item.url == it.url })
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Snackbar.make(view, it.unavailableMessage, Snackbar.LENGTH_SHORT).show() Snackbar.make(view, it.unavailableMessage, Snackbar.LENGTH_SHORT).show()
} }
} }
}else{ }else{
multipleFormatsListener.onFormatUpdated(it.url, it.formats) multipleFormatsListener.onFormatUpdated(it.url, it.formats)
items.filter { item -> item!!.url == it.url }.forEach { d -> items.filter { item -> item.url == it.url }.forEach { d ->
d?.allFormats?.clear() d.allFormats.clear()
d?.allFormats?.addAll(it.formats) d.allFormats.addAll(it.formats)
} }
progressInt++ progressInt++
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
progress = "${progressInt}/${itemsWithMissingFormats.size}" progress = "${progressInt}/${itemsWithMissingFormats.size}"
refreshBtn.text = progress refreshBtn.text = progress
} }
} }
}
} formatViewModel.setItems(items)
}
formatCollection.addAll(res) withContext(Dispatchers.Main){
filterBtn.isEnabled = true
if (!isActive) return@launch okBtn.isEnabled = true
}
val flatFormatCollection = formatCollection.flatten() }catch (e: Exception){
val commonFormats = withContext(Dispatchers.Main) {
flatFormatCollection.groupingBy { it.format_id }.eachCount() refreshBtn.isEnabled = true
.filter { it.value == distinctItems.size } filterBtn.isEnabled = true
.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } } okBtn.isEnabled = true
.map { it.value } refreshBtn.text = getString(R.string.update)
formats.addAll(commonFormats) recyclerView.isVisible = true
shimmers.visibility = View.GONE
chosenFormats = commonFormats.filter { it.filesize != 0L } shimmers.stopShimmer()
.mapTo(mutableListOf()) { it.copy() } e.printStackTrace()
chosenFormats = when (items.first()?.type) { Toast.makeText(context, getString(R.string.error_updating_formats), Toast.LENGTH_SHORT).show()
Type.audio -> chosenFormats.filter { }
it.vcodec.isBlank() || it.vcodec == "none" }
} }
updateFormatsJob?.start()
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()
}
} }
okBtn.setOnClickListener { okBtn.setOnClickListener {
@ -364,6 +314,7 @@ class FormatSelectionBottomSheetDialog(
filterSheet.setContentView(R.layout.format_category_sheet) filterSheet.setContentView(R.layout.format_category_sheet)
//format filter //format filter
val isMissingFormats = formatViewModel.isMissingFormats.value
filterSheet.findViewById<LinearLayout>(R.id.format_filter_linear)?.isVisible = !isMissingFormats filterSheet.findViewById<LinearLayout>(R.id.format_filter_linear)?.isVisible = !isMissingFormats
if (!isMissingFormats) { if (!isMissingFormats) {
val all = filterSheet.findViewById<TextView>(R.id.all) val all = filterSheet.findViewById<TextView>(R.id.all)
@ -373,7 +324,7 @@ class FormatSelectionBottomSheetDialog(
val filterOptions = listOf(all!!, suggested!!,smallest!!, generic!!) val filterOptions = listOf(all!!, suggested!!,smallest!!, generic!!)
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } 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.ALL -> all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
FormatCategory.SUGGESTED -> { FormatCategory.SUGGESTED -> {
suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
@ -388,37 +339,34 @@ class FormatSelectionBottomSheetDialog(
all.setOnClickListener { all.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.ALL
all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss() filterSheet.dismiss()
formatViewModel.filterBy.value = FormatCategory.ALL
} }
suggested.setOnClickListener { suggested.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.SUGGESTED
suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss() filterSheet.dismiss()
formatViewModel.filterBy.value = FormatCategory.SUGGESTED
} }
smallest.setOnClickListener { smallest.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.SMALLEST
smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss() filterSheet.dismiss()
formatViewModel.filterBy.value = FormatCategory.SMALLEST
} }
generic.setOnClickListener { generic.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.GENERIC
generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss() filterSheet.dismiss()
formatViewModel.filterBy.value = FormatCategory.GENERIC
} }
} }
//format source //format source
val formatSourceLinear = filterSheet.findViewById<LinearLayout>(R.id.format_source_linear)!! 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 formatSourceLinear.isVisible = canSwitch
if (canSwitch) { if (canSwitch) {
val formatSourceOptions = mutableListOf<TextView>() val formatSourceOptions = mutableListOf<TextView>()
@ -435,6 +383,7 @@ class FormatSelectionBottomSheetDialog(
txt.tag = tag txt.tag = tag
txt.setOnClickListener { txt.setOnClickListener {
currentFormatSource = it.tag.toString() currentFormatSource = it.tag.toString()
formatViewModel.filterBy.value = FormatCategory.ALL
refreshBtn.performClick() refreshBtn.performClick()
filterSheet.dismiss() 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() val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
filterSheet.behavior.peekHeight = displayMetrics.heightPixels filterSheet.behavior.peekHeight = displayMetrics.heightPixels
@ -482,240 +404,18 @@ class FormatSelectionBottomSheetDialog(
} }
private fun returnFormats(){ private fun returnFormats(){
if (items.size == 1){ if (_listener != null){
//simple video format selection //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{ }else{
//playlist format selection val res = formatViewModel.getFormatsForItemsBasedOnFormat(adapter.selectedVideoFormat!!, adapter.selectedAudioFormats)
val formatsToReturn = mutableListOf<MultipleItemFormatTuple>() multipleFormatsListener.onFormatClick(res)
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)
} }
} }
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) { override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog) super.onCancel(dialog)
cleanUp() cleanUp()
@ -733,6 +433,16 @@ class FormatSelectionBottomSheetDialog(
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("formatSheet")!!).commit() 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{ 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) val s = getString(R.string.video_recommendations_summary)
summary = if (value.isNullOrBlank()) { summary = if (value.isNullOrBlank()) {
s s
@ -288,13 +288,20 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
if (newValue == "yt_api") { if (newValue == "yt_api") {
findPreference<EditTextPreference>("api_key")?.isVisible = true findPreference<EditTextPreference>("api_key")?.isVisible = true
}else if (newValue == "custom") {
findPreference<EditTextPreference>("custom_home_recommendation_url")?.isVisible = true
} }
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 { 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) val s = getString(R.string.api_key_summary)
summary = if (text.isNullOrBlank()) { summary = if (text.isNullOrBlank()) {
s s

View file

@ -1,29 +1,45 @@
package com.deniscerri.ytdl.ui.more.settings.advanced.generateyoutubepotokens package com.deniscerri.ytdl.ui.more.settings.advanced.generateyoutubepotokens
import android.app.Activity import android.app.Activity
import android.content.Context.INPUT_METHOD_SERVICE
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.WorkManager import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R 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.YoutubeGeneratePoTokenItem
import com.deniscerri.ytdl.database.models.YoutubePoTokenItem 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.SettingsActivity
import com.deniscerri.ytdl.ui.more.settings.advanced.generateyoutubepotokens.webview.PoTokenWebViewLoginActivity 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.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.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.launch
class GenerateYoutubePoTokensFragment : Fragment() { class GenerateYoutubePoTokensFragment : Fragment() {
private lateinit var settingsActivity: SettingsActivity private lateinit var settingsActivity: SettingsActivity
@ -31,6 +47,8 @@ class GenerateYoutubePoTokensFragment : Fragment() {
private lateinit var configuration : MutableList<YoutubeGeneratePoTokenItem> private lateinit var configuration : MutableList<YoutubeGeneratePoTokenItem>
private lateinit var workManager : WorkManager private lateinit var workManager : WorkManager
private lateinit var webPoTokenResultLauncher : ActivityResultLauncher<Intent>
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -119,7 +137,7 @@ class GenerateYoutubePoTokensFragment : Fragment() {
.show() .show()
} }
val webPoTokenResultLauncher = registerForActivityResult( webPoTokenResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()
) { result -> ) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
@ -137,13 +155,11 @@ class GenerateYoutubePoTokensFragment : Fragment() {
configuration.add(conf) configuration.add(conf)
setValues(conf) setValues(conf)
preferences.edit().putString("youtube_generated_po_tokens", Gson().toJson(configuration).toString()).apply() 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 { regenerate.setOnClickListener {
webPoTokenResultLauncher.launch(Intent(requireContext(), PoTokenWebViewLoginActivity::class.java)) showBottomSheet()
} }
switch.setOnClickListener { 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) super.onCreate(savedInstanceState)
setContentView(R.layout.webview_activity) setContentView(R.layout.webview_activity)
val url = intent.getStringExtra("url")!!
cookiesViewModel = ViewModelProvider(this)[CookieViewModel::class.java] cookiesViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
lifecycleScope.launch { lifecycleScope.launch {
val appbar = findViewById<AppBarLayout>(R.id.webview_appbarlayout) val appbar = findViewById<AppBarLayout>(R.id.webview_appbarlayout)
@ -77,6 +79,7 @@ class PoTokenWebViewLoginActivity : BaseActivity() {
cookieManager = CookieManager.getInstance() cookieManager = CookieManager.getInstance()
preferences = PreferenceManager.getDefaultSharedPreferences(this@PoTokenWebViewLoginActivity) preferences = PreferenceManager.getDefaultSharedPreferences(this@PoTokenWebViewLoginActivity)
preferences.edit().putString("genenerate_youtube_po_token_preferred_url", url).apply()
webViewClient = object : AccompanistWebViewClient() { webViewClient = object : AccompanistWebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
@ -113,13 +116,13 @@ class PoTokenWebViewLoginActivity : BaseActivity() {
//update cookies //update cookies
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val url = "Po Token Generated Cookies" val cookieURL = "Po Token Generated Cookies"
cookiesViewModel.getCookiesFromDB(url).getOrNull()?.let { cookiesViewModel.getCookiesFromDB(cookieURL).getOrNull()?.let {
kotlin.runCatching { kotlin.runCatching {
cookiesViewModel.insert( cookiesViewModel.insert(
CookieItem( CookieItem(
0, 0,
url, cookieURL,
it it
) )
) )
@ -137,8 +140,6 @@ class PoTokenWebViewLoginActivity : BaseActivity() {
} }
} }
webView.clearCache(true) webView.clearCache(true)
// ensures that the WebView isn't doing anything when destroying it // ensures that the WebView isn't doing anything when destroying it
webView.loadUrl("about:blank") webView.loadUrl("about:blank")
@ -150,7 +151,7 @@ class PoTokenWebViewLoginActivity : BaseActivity() {
} }
webViewCompose.apply { webViewCompose.apply {
setContent { WebViewView() } setContent { WebViewView(url) }
} }
} }
@ -158,7 +159,7 @@ class PoTokenWebViewLoginActivity : BaseActivity() {
@SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") @SuppressLint("SetJavaScriptEnabled", "JavascriptInterface")
@Composable @Composable
fun WebViewView() { fun WebViewView(url: String) {
val webViewChromeClient = remember { val webViewChromeClient = remember {
object : AccompanistWebChromeClient() { object : AccompanistWebChromeClient() {
} }
@ -166,7 +167,7 @@ class PoTokenWebViewLoginActivity : BaseActivity() {
Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues -> Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues ->
WebView( WebView(
state = rememberWebViewState("https://youtube.com/account"), client = webViewClient, chromeClient = webViewChromeClient, state = rememberWebViewState(url), client = webViewClient, chromeClient = webViewChromeClient,
modifier = Modifier modifier = Modifier
.padding(paddingValues) .padding(paddingValues)
.fillMaxSize(), .fillMaxSize(),

View file

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

View file

@ -172,7 +172,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner" android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary" android:backgroundTint="?attr/colorPrimaryInverse"
android:clickable="false" android:clickable="false"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center" android:gravity="center"

View file

@ -102,54 +102,4 @@
</LinearLayout> </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> </LinearLayout>

View file

@ -2,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical" android:orientation="vertical"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@ -103,81 +104,18 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<androidx.core.widget.NestedScrollView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp" android:paddingTop="10dp"
android:scrollbars="none"> android:layout_height="wrap_content"
tools:listitem="@layout/format_item" />
<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>
<com.facebook.shimmer.ShimmerFrameLayout <com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/format_list_shimmer" android:id="@+id/format_list_shimmer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone"
android:orientation="vertical" android:orientation="vertical"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:paddingTop="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 <item
android:id="@+id/redownload" android:id="@+id/redownload"
android:icon="@drawable/baseline_share_24" android:icon="@drawable/baseline_download_24"
app:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:title="@string/redownload" /> android:title="@string/redownload" />

View file

@ -1151,6 +1151,7 @@
<item>@string/ytdlp_recommnedations</item> <item>@string/ytdlp_recommnedations</item>
<item>@string/ytdlp_liked</item> <item>@string/ytdlp_liked</item>
<item>@string/ytdlp_watch_history</item> <item>@string/ytdlp_watch_history</item>
<item>@string/custom</item>
</array> </array>
<array name="video_recommnedations_values"> <array name="video_recommnedations_values">
@ -1161,6 +1162,7 @@
<item>yt_dlp_recommendations</item> <item>yt_dlp_recommendations</item>
<item>yt_dlp_liked</item> <item>yt_dlp_liked</item>
<item>yt_dlp_watch_history</item> <item>yt_dlp_watch_history</item>
<item>custom</item>
</array> </array>
<!--below is for preferred audio language, not related to app's language--> <!--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="subtitle_languages">Subtitle languages</string>
<string name="format_source">Formats Source</string> <string name="format_source">Formats Source</string>
<string name="video_recommendations">Video Recommendations</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">Preferred Search Engine</string>
<string name="preferred_search_engine_summary">The search engine to use for in-app searches</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> <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="generate_potokens">Generate PO Tokens</string>
<string name="regenerate">Re-generate</string> <string name="regenerate">Re-generate</string>
<string name="generate_potokens_warning">* By enabling this, you need to disable cookies</string> <string name="generate_potokens_warning">* By enabling this, you need to disable cookies</string>
<string name="custom">Custom</string>
</resources> </resources>

View file

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