more fixes
This commit is contained in:
parent
db55894fdc
commit
1426ceef0a
24 changed files with 735 additions and 679 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ object UiUtil {
|
|||
if (chosenFormat.tbr.isNullOrBlank() || (chosenFormat.vcodec.isNotBlank() && chosenFormat.vcodec != "none")) {
|
||||
isVisible = false
|
||||
}else{
|
||||
isVisible = true
|
||||
text = chosenFormat.tbr
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
15
app/src/main/res/layout/format_type_label.xml
Normal file
15
app/src/main/res/layout/format_type_label.xml
Normal 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" />
|
||||
|
|
@ -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>
|
||||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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-->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue