Add multiselect audio in format card. use piped for format updating and single video fetching, the rest invidious
Make search suggestions as a preference cuz of privacy reasons
Selecting the videos in the download queue causes the app to be force closed
Removed ip whois network request
update download item data when you schedule a quick downloaded item
This commit is contained in:
deniscerri 2023-05-22 20:32:09 +02:00
parent 35f8f939e8
commit aadac439f3
No known key found for this signature in database
GPG key ID: 95C43D517D830350
28 changed files with 630 additions and 211 deletions

View file

@ -198,7 +198,7 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
} }
override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean { override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
return oldItem.url == newItem.url && oldItem.format.format_id == newItem.format.format_id return oldItem.id == newItem.id && oldItem.title == newItem.title && oldItem.author == newItem.author && oldItem.thumb == newItem.thumb
} }
} }
} }

View file

@ -43,6 +43,9 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1")
fun getDownloadById(id: Long) : DownloadItem fun getDownloadById(id: Long) : DownloadItem
@Query("SELECT id FROM downloads ORDER BY id DESC LIMIT 1")
fun getLastDownloadId(): Long
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(item: DownloadItem) : Long suspend fun insert(item: DownloadItem) : Long

View file

@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
data class Format( data class Format(
@SerializedName(value = "format_id", alternate = ["itag"]) @SerializedName(value = "format_id", alternate = ["itag"])
var format_id: String = "", var format_id: String = "",
@SerializedName(value = "ext", alternate = ["container"]) @SerializedName(value = "ext", alternate = ["container", "format"])
var container: String = "", var container: String = "",
@SerializedName(value = "vcodec") @SerializedName(value = "vcodec")
var vcodec: String = "", var vcodec: String = "",
@ -15,7 +15,7 @@ data class Format(
var encoding: String = "", var encoding: String = "",
@SerializedName(value = "filesize", alternate = ["clen", "filesize_approx"]) @SerializedName(value = "filesize", alternate = ["clen", "filesize_approx"])
var filesize: Long = 0, var filesize: Long = 0,
@SerializedName(value = "format_note", alternate = ["resolution", "audioQuality"]) @SerializedName(value = "format_note", alternate = ["resolution", "audioQuality", "quality"])
var format_note: String = "", var format_note: String = "",
@SerializedName(value = "fps") @SerializedName(value = "fps")
var fps: String? = "", var fps: String? = "",

View file

@ -7,5 +7,6 @@ data class VideoPreferences (
var sponsorBlockFilters: ArrayList<String> = arrayListOf(), var sponsorBlockFilters: ArrayList<String> = arrayListOf(),
var writeSubs: Boolean = false, var writeSubs: Boolean = false,
var subsLanguages: String = "en.*,.*-orig", var subsLanguages: String = "en.*,.*-orig",
var audioFormatIDs : ArrayList<String> = arrayListOf(),
var removeAudio: Boolean = false var removeAudio: Boolean = false
) )

View file

@ -72,6 +72,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.cancelQueued() downloadDao.cancelQueued()
} }
fun getLastDownloadId() : Long {
return downloadDao.getLastDownloadId()
}
fun checkIfReDownloadingErroredOrCancelled(item: DownloadItem) : Long { fun checkIfReDownloadingErroredOrCancelled(item: DownloadItem) : Long {
val converters = Converters() val converters = Converters()

View file

@ -4,6 +4,7 @@ import android.app.Application
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@ -28,9 +29,12 @@ import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.models.VideoPreferences import com.deniscerri.ytdlnis.database.models.VideoPreferences
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.work.DownloadWorker import com.deniscerri.ytdlnis.work.DownloadWorker
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -39,6 +43,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
private val repository : DownloadRepository private val repository : DownloadRepository
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao private val commandTemplateDao: CommandTemplateDao
private val infoUtil : InfoUtil
val allDownloads : LiveData<List<DownloadItem>> val allDownloads : LiveData<List<DownloadItem>>
val queuedDownloads : LiveData<List<DownloadItem>> val queuedDownloads : LiveData<List<DownloadItem>>
val activeDownloads : LiveData<List<DownloadItem>> val activeDownloads : LiveData<List<DownloadItem>>
@ -62,6 +67,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository = DownloadRepository(dao) repository = DownloadRepository(dao)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
infoUtil = InfoUtil(application)
allDownloads = repository.allDownloads.asLiveData() allDownloads = repository.allDownloads.asLiveData()
queuedDownloads = repository.queuedDownloads.asLiveData() queuedDownloads = repository.queuedDownloads.asLiveData()
@ -74,9 +80,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString() videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString()
formatIDPreference = sharedPreferences.getString("format_id", "").toString() formatIDPreference = sharedPreferences.getString("format_id", "").toString()
val videoFormat = getApplication<App>().resources.getStringArray(R.array.video_formats) val videoFormat = App.instance.resources.getStringArray(R.array.video_formats)
var videoContainer = sharedPreferences.getString("video_format", "Default") var videoContainer = sharedPreferences.getString("video_format", "Default")
if (videoContainer == "Default") videoContainer = application.getString(R.string.defaultValue) if (videoContainer == "Default") videoContainer = App.instance.getString(R.string.defaultValue)
defaultVideoFormats = mutableListOf() defaultVideoFormats = mutableListOf()
videoFormat.forEach { videoFormat.forEach {
@ -95,7 +101,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
bestVideoFormat = defaultVideoFormats.last() bestVideoFormat = defaultVideoFormats.last()
var audioContainer = sharedPreferences.getString("audio_format", "mp3") var audioContainer = sharedPreferences.getString("audio_format", "mp3")
if (audioContainer == "Default") audioContainer = application.getString(R.string.defaultValue) if (audioContainer == "Default") audioContainer = App.instance.getString(R.string.defaultValue)
bestAudioFormat = Format( bestAudioFormat = Format(
getApplication<App>().resources.getString(R.string.best_quality), getApplication<App>().resources.getString(R.string.best_quality),
audioContainer!!, audioContainer!!,
@ -112,7 +118,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun updateDownload(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){ fun updateDownload(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){
repository.update(item); repository.update(item)
} }
fun getItemByID(id: Long) : DownloadItem { fun getItemByID(id: Long) : DownloadItem {
@ -317,20 +323,20 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun getGenericAudioFormats() : MutableList<Format>{ fun getGenericAudioFormats() : MutableList<Format>{
val audioFormats = application.resources.getStringArray(R.array.audio_formats) val audioFormats = App.instance.resources.getStringArray(R.array.audio_formats)
val formats = mutableListOf<Format>() val formats = mutableListOf<Format>()
var containerPreference = sharedPreferences.getString("audio_format", "Default") var containerPreference = sharedPreferences.getString("audio_format", "Default")
if (containerPreference == "Default") containerPreference = application.getString(R.string.defaultValue) if (containerPreference == "Default") containerPreference = App.instance.getString(R.string.defaultValue)
audioFormats.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) } audioFormats.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) }
return formats return formats
} }
fun getGenericVideoFormats() : MutableList<Format>{ fun getGenericVideoFormats() : MutableList<Format>{
val videoFormats = application.resources.getStringArray(R.array.video_formats) val videoFormats = App.instance.resources.getStringArray(R.array.video_formats)
val formats = mutableListOf<Format>() val formats = mutableListOf<Format>()
var containerPreference = sharedPreferences.getString("video_format", "Default") var containerPreference = sharedPreferences.getString("video_format", "Default")
if (containerPreference == "Default") containerPreference = application.getString(R.string.defaultValue) if (containerPreference == "Default") containerPreference = application.getString(R.string.defaultValue)
videoFormats.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) } videoFormats.forEach { formats.add(Format(it, containerPreference!!,application.getString(R.string.defaultValue),"", "",0, it)) }
return formats return formats
} }
@ -385,14 +391,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return repository.getActiveAndQueuedDownloads() return repository.getActiveAndQueuedDownloads()
} }
suspend fun queueDownloads(items: List<DownloadItem>) { suspend fun queueDownloads(items: List<DownloadItem>) = CoroutineScope(Dispatchers.IO).launch {
val context = getApplication<App>().applicationContext val context = App.instance
val activeAndQueuedDownloads = withContext(Dispatchers.IO){ val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads()
repository.getActiveAndQueuedDownloads()
}
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true) val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val queuedItems = mutableListOf<DownloadItem>() val queuedItems = mutableListOf<DownloadItem>()
var lastDownloadId = repository.getLastDownloadId()
items.forEach { items.forEach {
lastDownloadId++
it.status = DownloadRepository.Status.Queued.toString() it.status = DownloadRepository.Status.Queued.toString()
if (activeAndQueuedDownloads.firstOrNull{d -> if (activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0 d.id = 0
@ -401,12 +407,32 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} != null) { } != null) {
Toast.makeText(context, context.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show() Toast.makeText(context, context.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
}else{ }else{
it.id = withContext(Dispatchers.IO){ it.id = lastDownloadId
repository.insert(it) val insert = async {repository.insert(it)}
} val id = insert.await()
it.id = id
queuedItems.add(it) queuedItems.add(it)
} }
} }
//if the download was quick downloaded and are incomplete
if (queuedItems.count() == 1){
val item = queuedItems.first()
if (item.title.isEmpty() || item.author.isEmpty() || item.thumb.isEmpty()){
runCatching {
val info = infoUtil.getMissingInfo(item.url)
if (item.title.isEmpty()) item.title = info?.title.toString()
if (item.author.isEmpty()) item.author = info?.author.toString()
item.duration = info?.duration.toString()
item.website = info?.website.toString()
if (item.thumb.isEmpty()) item.thumb = info?.thumb.toString()
repository.update(item)
}.onFailure { err ->
Log.e("error", err.message.toString())
}
}
}
queuedItems.forEach { queuedItems.forEach {
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
var delay = if (it.downloadStartTime != 0L){ var delay = if (it.downloadStartTime != 0L){

View file

@ -376,13 +376,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}else{ }else{
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_plus, 0) searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_plus, 0)
} }
val suggestions = withContext(Dispatchers.IO){ val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){
if (it!!.isEmpty()) { withContext(Dispatchers.IO){
resultViewModel.getSearchHistory().map { it.query } if (it!!.isEmpty()) {
}else{ resultViewModel.getSearchHistory().map { it.query }
infoUtil!!.getSearchSuggestions(it.toString()) }else{
infoUtil!!.getSearchSuggestions(it.toString())
}
} }
} } else emptyList()
if (it!!.isEmpty()){ if (it!!.isEmpty()){
for (i in suggestions.indices) { for (i in suggestions.indices) {
@ -445,8 +447,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
} }
searchSuggestionsLinearLayout!!.visibility = VISIBLE searchSuggestionsLinearLayout!!.visibility = VISIBLE
} }
} }
} }

View file

@ -35,6 +35,7 @@ import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
@ -141,13 +142,13 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
if (formats.isEmpty()) formats = downloadViewModel.getGenericAudioFormats() if (formats.isEmpty()) formats = downloadViewModel.getGenericAudioFormats()
val formatCard = view.findViewById<ConstraintLayout>(R.id.format_card_constraintLayout) val formatCard = view.findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
val chosenFormat = downloadItem.format val chosenFormat = downloadItem.format
uiUtil.populateFormatCard(formatCard, chosenFormat) uiUtil.populateFormatCard(formatCard, chosenFormat, null)
val listener = object : OnFormatClickListener { val listener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, item: List<Format>) { override fun onFormatClick(allFormats: List<List<Format>>, item: List<Format>) {
downloadItem.format = item.first() downloadItem.format = item.first()
uiUtil.populateFormatCard(formatCard, item.first()) uiUtil.populateFormatCard(formatCard, item.first(), null)
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
resultItem.formats.removeAll(formats.toSet()) resultItem.formats.removeAll(formats.toSet())
@ -156,6 +157,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
} }
} }
formats = allFormats.first().toMutableList() formats = allFormats.first().toMutableList()
downloadItem.format.container = container.editText?.text.toString()
} }
} }
formatCard.setOnClickListener{ formatCard.setOnClickListener{

View file

@ -7,6 +7,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -28,6 +29,7 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.receiver.ShareActivity import com.deniscerri.ytdlnis.receiver.ShareActivity
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@ -47,6 +49,7 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var commandTemplateDao : CommandTemplateDao private lateinit var commandTemplateDao : CommandTemplateDao
private lateinit var uiUtil: UiUtil private lateinit var uiUtil: UiUtil
private lateinit var infoUtil: InfoUtil
private lateinit var downloadAudioFragment: DownloadAudioFragment private lateinit var downloadAudioFragment: DownloadAudioFragment
private lateinit var downloadVideoFragment: DownloadVideoFragment private lateinit var downloadVideoFragment: DownloadVideoFragment
@ -58,6 +61,7 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao
uiUtil = UiUtil(FileUtil()) uiUtil = UiUtil(FileUtil())
infoUtil = InfoUtil(requireContext())
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")

View file

@ -36,6 +36,7 @@ import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
@ -133,8 +134,8 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
)) ))
var formats = mutableListOf<Format>() var formats = mutableListOf<Format>()
formats.addAll(resultItem.formats.filter { !it.format_note.contains("audio", ignoreCase = true) }) formats.addAll(resultItem.formats)
if (formats.isEmpty()) formats.addAll(downloadItem.allFormats.filter { !it.format_note.contains("audio", ignoreCase = true) }) if (formats.isEmpty()) formats.addAll(downloadItem.allFormats)
val containers = requireContext().resources.getStringArray(R.array.video_containers) val containers = requireContext().resources.getStringArray(R.array.video_containers)
val container = view.findViewById<TextInputLayout>(R.id.downloadContainer) val container = view.findViewById<TextInputLayout>(R.id.downloadContainer)
@ -145,13 +146,14 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
if (formats.isEmpty()) formats = downloadViewModel.getGenericVideoFormats() if (formats.isEmpty()) formats = downloadViewModel.getGenericVideoFormats()
val formatCard = view.findViewById<ConstraintLayout>(R.id.format_card_constraintLayout) val formatCard = view.findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
val chosenFormat = downloadItem.format val chosenFormat = downloadItem.format
uiUtil.populateFormatCard(formatCard, chosenFormat) uiUtil.populateFormatCard(formatCard, chosenFormat, null)
val listener = object : OnFormatClickListener { val listener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, item: List<Format>) { override fun onFormatClick(allFormats: List<List<Format>>, item: List<Format>) {
downloadItem.format = item.first() downloadItem.format = item.first()
downloadItem.videoPreferences.audioFormatIDs.addAll(item.drop(1).map { it.format_id })
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
resultItem.formats.removeAll(formats.toSet()) resultItem.formats.removeAll(formats.toSet())
@ -160,7 +162,8 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
} }
} }
formats = allFormats.first().toMutableList() formats = allFormats.first().toMutableList()
uiUtil.populateFormatCard(formatCard, item.first()) uiUtil.populateFormatCard(formatCard, item.first(), item.drop(1).map { it.format_note })
downloadItem.format.container = container.editText?.text.toString()
} }
} }
formatCard.setOnClickListener{ formatCard.setOnClickListener{

View file

@ -8,7 +8,7 @@ import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.forEach
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -22,6 +22,8 @@ import com.deniscerri.ytdlnis.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.card.MaterialCardView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -36,6 +38,14 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var formatCollection: MutableList<List<Format>> private lateinit var formatCollection: MutableList<List<Format>>
private lateinit var chosenFormats: List<Format> private lateinit var chosenFormats: List<Format>
private lateinit var selectedVideo : Format
private lateinit var selectedAudios : MutableList<Format>
private lateinit var videoFormatList : LinearLayout
private lateinit var audioFormatList : LinearLayout
private lateinit var okBtn : Button
private lateinit var videoTitle : TextView
private lateinit var audioTitle : TextView
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -44,6 +54,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
infoUtil = InfoUtil(requireActivity().applicationContext) infoUtil = InfoUtil(requireActivity().applicationContext)
formatCollection = mutableListOf() formatCollection = mutableListOf()
chosenFormats = listOf() chosenFormats = listOf()
selectedAudios = mutableListOf()
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
} }
@ -61,8 +72,15 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
behavior.peekHeight = displayMetrics.heightPixels / 2 behavior.peekHeight = displayMetrics.heightPixels / 2
} }
val linearLayout = view.findViewById<LinearLayout>(R.id.format_list_linear_layout) val formatListLinearLayout = view.findViewById<LinearLayout>(R.id.format_list_linear_layout)
val shimmers = view.findViewById<ShimmerFrameLayout>(R.id.format_list_shimmer) 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 shimmers.visibility = View.GONE
val hasGenericFormats = when(items.first()!!.type){ val hasGenericFormats = when(items.first()!!.type){
Type.audio -> formats.first().size == resources.getStringArray(R.array.audio_formats).size Type.audio -> formats.first().size == resources.getStringArray(R.array.audio_formats).size
@ -87,16 +105,15 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
}else{ }else{
chosenFormats = formats.flatten() chosenFormats = formats.flatten()
} }
addFormatsToView(linearLayout) addFormatsToView()
}else{ }else{
chosenFormats = formats.flatten() chosenFormats = formats.flatten()
if(!hasGenericFormats){ if(!hasGenericFormats){
chosenFormats = when(items.first()?.type){ if(items.first()?.type == Type.audio){
Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) } chosenFormats = chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
else -> chosenFormats.filter { !it.format_note.contains("audio", ignoreCase = true) }
} }
} }
addFormatsToView(linearLayout) addFormatsToView()
} }
val refreshBtn = view.findViewById<Button>(R.id.format_refresh) val refreshBtn = view.findViewById<Button>(R.id.format_refresh)
@ -107,7 +124,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
lifecycleScope.launch { lifecycleScope.launch {
try { try {
refreshBtn.isEnabled = false refreshBtn.isEnabled = false
linearLayout.visibility = View.GONE formatListLinearLayout.visibility = View.GONE
shimmers.visibility = View.VISIBLE shimmers.visibility = View.VISIBLE
shimmers.startShimmer() shimmers.startShimmer()
@ -117,9 +134,8 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
infoUtil.getFormats(items.first()!!.url) infoUtil.getFormats(items.first()!!.url)
} }
chosenFormats = res.formats.filter { it.filesize != 0L } chosenFormats = res.formats.filter { it.filesize != 0L }
chosenFormats = when(items.first()?.type){ if(items.first()?.type == Type.audio){
Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) } chosenFormats = chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
else -> chosenFormats.filter { !it.format_note.contains("audio", ignoreCase = true) }
} }
if (chosenFormats.isEmpty()) throw Exception() if (chosenFormats.isEmpty()) throw Exception()
//playlist format filtering //playlist format filtering
@ -150,18 +166,16 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
.sumOf { itt -> itt.filesize } .sumOf { itt -> itt.filesize }
} }
} }
addFormatsToView(linearLayout)
refreshBtn.visibility = View.GONE
linearLayout.visibility = View.VISIBLE
shimmers.visibility = View.GONE shimmers.visibility = View.GONE
shimmers.stopShimmer() shimmers.stopShimmer()
addFormatsToView()
refreshBtn.visibility = View.GONE
formatListLinearLayout.visibility = View.VISIBLE
}catch (e: Exception){ }catch (e: Exception){
runCatching { runCatching {
refreshBtn.isEnabled = true refreshBtn.isEnabled = true
refreshBtn.text = getString(R.string.update_formats) refreshBtn.text = getString(R.string.update_formats)
linearLayout.visibility = View.VISIBLE formatListLinearLayout.visibility = View.VISIBLE
shimmers.visibility = View.GONE shimmers.visibility = View.GONE
shimmers.stopShimmer() shimmers.stopShimmer()
@ -171,38 +185,96 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
} }
} }
} }
okBtn.setOnClickListener {
val selectedFormats = mutableListOf<Format>()
selectedFormats.add(selectedVideo)
selectedFormats.addAll(selectedAudios)
listener.onFormatClick(List(items.size){chosenFormats}, selectedFormats)
dismiss()
}
if (sharedPreferences.getBoolean("update_formats", false) && refreshBtn.isVisible && items.size == 1){ if (sharedPreferences.getBoolean("update_formats", false) && refreshBtn.isVisible && items.size == 1){
refreshBtn.performClick() refreshBtn.performClick()
} }
} }
private fun addFormatsToView(linearLayout: LinearLayout){ private fun addFormatsToView(){
linearLayout.removeAllViews() val isSingleAndVideo = items.first()?.type == Type.video && items.count() == 1
videoFormatList.removeAllViews()
audioFormatList.removeAllViews()
if (!isSingleAndVideo) {
audioFormatList.visibility = View.GONE
videoTitle.visibility = View.GONE
audioTitle.visibility = View.GONE
okBtn.visibility = View.GONE
}else{
if (chosenFormats.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
}
}
for (i in chosenFormats.lastIndex downTo 0){ for (i in chosenFormats.lastIndex downTo 0){
val format = chosenFormats[i] val format = chosenFormats[i]
val formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null) val formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null)
uiUtil.populateFormatCard(formatItem as ConstraintLayout, format) formatItem.tag = "${format.format_id}${format.format_note}"
formatItem.setOnClickListener{_ -> uiUtil.populateFormatCard(formatItem as MaterialCardView, format, null)
if (items.size == 1){ formatItem.setOnClickListener{ clickedformat ->
listener.onFormatClick(List(items.size){chosenFormats}, listOf(format)) //if the context is behind a single download card and its a video, allow the ability to multiselect audio formats
}else{ if (isSingleAndVideo){
val selectedFormats = mutableListOf<Format>() val clickedCard = (clickedformat as MaterialCardView)
formatCollection.forEach { if (format.vcodec.isNotBlank() && format.vcodec != "none") {
selectedFormats.add(it.first{ f -> f.format_id == format.format_id}) videoFormatList.forEach { (it as MaterialCardView).isChecked = false }
} selectedVideo = format
if (selectedFormats.isEmpty()) { clickedCard.isChecked = true
items.forEach { }else{
selectedFormats.add(format) if(selectedAudios.contains(format)) {
selectedAudios.remove(format)
} else {
selectedAudios.add(format)
} }
} }
listener.onFormatClick(formatCollection, selectedFormats) 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(List(items.size){chosenFormats}, listOf(format))
}else{
val selectedFormats = mutableListOf<Format>()
formatCollection.forEach {
selectedFormats.add(it.first{ f -> f.format_id == format.format_id})
}
if (selectedFormats.isEmpty()) {
items.forEach {
selectedFormats.add(format)
}
}
listener.onFormatClick(formatCollection, selectedFormats)
}
dismiss()
} }
dismiss()
} }
formatItem.setOnLongClickListener { formatItem.setOnLongClickListener {
uiUtil.showFormatDetails(format, requireActivity()) uiUtil.showFormatDetails(format, requireActivity())
true true
} }
linearLayout.addView(formatItem)
if (isSingleAndVideo){
if (format.vcodec.isNotBlank() && format.vcodec != "none") videoFormatList.addView(formatItem)
else audioFormatList.addView(formatItem)
}else{
videoFormatList.addView(formatItem)
}
} }
} }

View file

@ -9,12 +9,16 @@ import android.os.Bundle
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -55,6 +59,8 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
private lateinit var queuedRecyclerView : RecyclerView private lateinit var queuedRecyclerView : RecyclerView
private lateinit var queuedDownloads : GenericDownloadAdapter private lateinit var queuedDownloads : GenericDownloadAdapter
private lateinit var notificationUtil: NotificationUtil private lateinit var notificationUtil: NotificationUtil
private var selectedObjects: ArrayList<DownloadItem>? = null
private var actionMode : ActionMode? = null
private lateinit var items : MutableList<DownloadItem> private lateinit var items : MutableList<DownloadItem>
private lateinit var fileUtil: FileUtil private lateinit var fileUtil: FileUtil
private lateinit var uiUtil: UiUtil private lateinit var uiUtil: UiUtil
@ -70,6 +76,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
notificationUtil = NotificationUtil(requireContext()) notificationUtil = NotificationUtil(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
items = mutableListOf() items = mutableListOf()
selectedObjects = arrayListOf()
fileUtil = FileUtil() fileUtil = FileUtil()
uiUtil = UiUtil(fileUtil) uiUtil = UiUtil(fileUtil)
return fragmentView return fragmentView
@ -226,7 +233,23 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
} }
override fun onCardSelect(itemID: Long, isChecked: Boolean) { override fun onCardSelect(itemID: Long, isChecked: Boolean) {
TODO("Not yet implemented") val item = items.find { it.id == itemID }
if (isChecked) {
selectedObjects!!.add(item!!)
if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
}
}
else {
selectedObjects!!.remove(item)
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (selectedObjects!!.isEmpty()){
actionMode?.finish()
}
}
} }
private fun removeItem(itemID: Long){ private fun removeItem(itemID: Long){
@ -251,6 +274,76 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
deleteDialog.show() deleteDialog.show()
} }
private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.queued_menu_context, menu)
mode.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
return true
}
override fun onPrepareActionMode(
mode: ActionMode?,
menu: Menu?
): Boolean {
return false
}
override fun onActionItemClicked(
mode: ActionMode?,
item: MenuItem?
): Boolean {
return when (item!!.itemId) {
R.id.delete_results -> {
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){
val id = obj.id.toInt()
YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(requireContext()).cancelUniqueWork(id.toString())
notificationUtil.cancelDownloadNotification(id)
downloadViewModel.deleteDownload(obj)
}
clearCheckedItems()
actionMode?.finish()
}
deleteDialog.show()
true
}
R.id.select_all -> {
queuedDownloads.checkAll(items)
selectedObjects?.clear()
items.forEach { selectedObjects?.add(it) }
mode?.title = getString(R.string.all_items_selected)
true
}
R.id.invert_selected -> {
queuedDownloads.invertSelected(items)
val invertedList = arrayListOf<DownloadItem>()
items.forEach {
if (!selectedObjects?.contains(it)!!) invertedList.add(it)
}
selectedObjects?.clear()
selectedObjects?.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
true
}
else -> false
}
}
override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null
clearCheckedItems()
}
}
private fun clearCheckedItems(){
queuedDownloads.clearCheckeditems()
selectedObjects?.clear()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback = private var simpleCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) { object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder

View file

@ -62,6 +62,9 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
private lateinit var cookieViewModel: CookieViewModel private lateinit var cookieViewModel: CookieViewModel
private lateinit var commandTemplateViewModel: CommandTemplateViewModel private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private var version: Preference? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey) setPreferencesFromResource(R.xml.root_preferences, rootKey)
val navController = findNavController() val navController = findNavController()
@ -192,6 +195,24 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
appRestoreResultLauncher.launch(intent) appRestoreResultLauncher.launch(intent)
true true
} }
version = findPreference("version")
version!!.summary = BuildConfig.VERSION_NAME
version!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
lifecycleScope.launch{
withContext(Dispatchers.IO){
updateUtil!!.updateApp{ msg ->
lifecycleScope.launch(Dispatchers.Main){
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show()
}
}
}
}
true
}
} }
private fun backupSettings(preferences: SharedPreferences) : JsonArray { private fun backupSettings(preferences: SharedPreferences) : JsonArray {

View file

@ -18,7 +18,6 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.updating override val title: Int = R.string.updating
private var updateYTDL: Preference? = null private var updateYTDL: Preference? = null
private var ytdlVersion: Preference? = null private var ytdlVersion: Preference? = null
private var version: Preference? = null
private var updateUtil: UpdateUtil? = null private var updateUtil: UpdateUtil? = null
@ -30,8 +29,6 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
val editor = preferences.edit() val editor = preferences.edit()
updateYTDL = findPreference("update_ytdl") updateYTDL = findPreference("update_ytdl")
ytdlVersion = findPreference("ytdl-version") ytdlVersion = findPreference("ytdl-version")
version = findPreference("version")
version!!.summary = BuildConfig.VERSION_NAME
YoutubeDL.getInstance().version(context)?.let { YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it) editor.putString("ytdl-version", it)
@ -69,20 +66,6 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
true true
} }
version!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
lifecycleScope.launch{
withContext(Dispatchers.IO){
updateUtil!!.updateApp{ msg ->
lifecycleScope.launch(Dispatchers.Main){
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show()
}
}
}
}
true
}
} }

View file

@ -9,20 +9,26 @@ import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.google.common.net.HttpHeaders.USER_AGENT
import com.google.gson.Gson import com.google.gson.Gson
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.BufferedReader import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.IOException
import java.io.InputStreamReader import java.io.InputStreamReader
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
class InfoUtil(private val context: Context) { class InfoUtil(private val context: Context) {
private var items: ArrayList<ResultItem?> private var items: ArrayList<ResultItem?>
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
@ -42,16 +48,10 @@ class InfoUtil(private val context: Context) {
private fun init(){ private fun init(){
if (countryCODE.isEmpty()){ if (countryCODE.isEmpty()){
val country = genericRequest("https://ipwho.is/") countryCODE = "US"
countryCODE = try {
country.getString("country_code")
} catch (e: Exception) {
"US"
}
} }
if (!useInvidous){ if (!useInvidous){
val res = genericRequest(invidousURL + "stats") val res = genericRequest(invidousURL + "stats")
Log.e("Assa", res.toString())
useInvidous = res.length() != 0 useInvidous = res.length() != 0
} }
} }
@ -235,14 +235,10 @@ class InfoUtil(private val context: Context) {
try { try {
init() init()
if (key!!.isEmpty()) { if (key!!.isEmpty()) {
return if (useInvidous) { val res = genericRequest(pipedURL + "streams/" + id)
val res = genericRequest(invidousURL + "videos/" + id) return if (res.length() == 0) getFromYTDL("https://www.youtube.com/watch?v=$id")[0] else createVideoFromPipedJSON(
if (res.length() == 0) getFromYTDL("https://www.youtube.com/watch?v=$id")[0] else createVideoFromInvidiousJSON( res, id
res )
)
} else {
getFromYTDL("https://www.youtube.com/watch?v=$id")[0]
}
} }
//short data //short data
@ -345,6 +341,82 @@ class InfoUtil(private val context: Context) {
return video return video
} }
private fun createVideoFromPipedJSON(obj: JSONObject, id: String): ResultItem? {
var video: ResultItem? = null
try {
val title = obj.getString("title").toString()
title.replace("&#39;s", "'")
title.replace("&amp", "&")
val author = obj.getString("uploader").toString()
author.replace("&#39;s", "'")
author.replace("&amp", "&")
if (author.isBlank()) throw Exception()
val duration = formatIntegerDuration(obj.getInt("duration"), Locale.getDefault())
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
val url = "https://www.youtube.com/watch?v=$id"
val formats : ArrayList<Format> = ArrayList()
if (obj.has("audioStreams")){
val formatsInJSON = obj.getJSONArray("audioStreams")
for (f in 0 until formatsInJSON.length()){
val format = formatsInJSON.getJSONObject(f)
if (format.getInt("bitrate") == 0) continue
val formatObj = Gson().fromJson(format.toString(), Format::class.java)
try{
formatObj.acodec = format.getString("codec")
val streamURL = format.getString("url")
formatObj.format_id = streamURL.substringAfter("itag=").substringBefore("&")
formatObj.filesize = (format.getInt("bitrate") / 8 * obj.getInt("duration")).toLong()
formatObj.asr = format.getString("quality")
if (! format.getString("audioTrackName").equals("null", ignoreCase = true)){
formatObj.format_note = format.getString("audioTrackName") + " Audio, " + formatObj.format_note
}else{
formatObj.format_note = formatObj.format_note + " Audio"
}
}catch (e: Exception) {
e.printStackTrace()
}
formats.add(formatObj)
}
}
if (obj.has("videoStreams")){
val formatsInJSON = obj.getJSONArray("videoStreams")
for (f in 0 until formatsInJSON.length()){
val format = formatsInJSON.getJSONObject(f)
if (format.getInt("bitrate") == 0) continue
val formatObj = Gson().fromJson(format.toString(), Format::class.java)
try{
formatObj.vcodec = format.getString("codec")
val streamURL = format.getString("url")
formatObj.format_id = streamURL.substringAfter("itag=").substringBefore("&")
formatObj.filesize = (format.getInt("bitrate") / 8 * obj.getInt("duration")).toLong()
}catch (e: Exception) {
e.printStackTrace()
}
formats.add(formatObj)
}
}
formats.sortBy { it.filesize }
video = ResultItem(0,
url,
title,
author,
duration,
thumb,
"youtube",
"",
formats
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
return video
}
private fun genericRequest(url: String): JSONObject { private fun genericRequest(url: String): JSONObject {
Log.e(TAG, url) Log.e(TAG, url)
val reader: BufferedReader val reader: BufferedReader
@ -507,12 +579,12 @@ class InfoUtil(private val context: Context) {
val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?") val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?")
val m = p.matcher(url) val m = p.matcher(url)
val formatSource = sharedPreferences.getString("formats_source", "yt-dlp") val formatSource = sharedPreferences.getString("formats_source", "yt-dlp")
return if(m.find() && useInvidous && formatSource == "invidious"){ return if(m.find() && formatSource == "piped"){
val id = getIDFromYoutubeURL(url) val id = getIDFromYoutubeURL(url)
val res = genericRequest(invidousURL + "videos/" + id) val res = genericRequest(pipedURL + "streams/" + id)
if (res.length() == 0) getFromYTDL(url)[0]!! if (res.length() == 0) getFromYTDL(url)[0]!!
else createVideoFromInvidiousJSON( else createVideoFromPipedJSON(
res res, id
)!! )!!
}else{ }else{
getFromYTDL(url)[0]!! getFromYTDL(url)[0]!!
@ -583,8 +655,8 @@ class InfoUtil(private val context: Context) {
}else{ }else{
urls.forEach { urls.forEach {
val id = getIDFromYoutubeURL(it) val id = getIDFromYoutubeURL(it)
val res = genericRequest(invidousURL + "videos/" + id) val res = genericRequest(pipedURL + "streams/" + id)
val vid = createVideoFromInvidiousJSON(res) val vid = createVideoFromPipedJSON(res, id)
progress(vid!!.formats) progress(vid!!.formats)
} }
} }
@ -881,6 +953,7 @@ class InfoUtil(private val context: Context) {
companion object { companion object {
private const val TAG = "API MANAGER" private const val TAG = "API MANAGER"
private const val invidousURL = "https://invidious.nerdvpn.de/api/v1/" private const val invidousURL = "https://invidious.nerdvpn.de/api/v1/"
private const val pipedURL = "https://pipedapi.kavin.rocks/"
private var countryCODE: String = "" private var countryCODE: String = ""
} }
} }

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.util package com.deniscerri.ytdlnis.util
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
@ -30,6 +31,7 @@ import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.datepicker.CalendarConstraints import com.google.android.material.datepicker.CalendarConstraints
@ -44,9 +46,15 @@ import java.io.File
import java.util.Calendar import java.util.Calendar
class UiUtil(private val fileUtil: FileUtil) { class UiUtil(private val fileUtil: FileUtil) {
fun populateFormatCard(formatCard : ConstraintLayout, chosenFormat: Format){ @SuppressLint("SetTextI18n")
fun populateFormatCard(formatCard : MaterialCardView, chosenFormat: Format, audioFormats: List<String>?){
formatCard.findViewById<TextView>(R.id.container).text = chosenFormat.container.uppercase() formatCard.findViewById<TextView>(R.id.container).text = chosenFormat.container.uppercase()
formatCard.findViewById<TextView>(R.id.format_note).text = chosenFormat.format_note.uppercase() if (audioFormats.isNullOrEmpty()){
formatCard.findViewById<TextView>(R.id.format_note).text = chosenFormat.format_note.uppercase()
}else{
val title = "${chosenFormat.format_note.uppercase()} + [${audioFormats.joinToString("/")}]"
formatCard.findViewById<TextView>(R.id.format_note).text = title
}
formatCard.findViewById<TextView>(R.id.format_id).text = "id: ${chosenFormat.format_id}" formatCard.findViewById<TextView>(R.id.format_id).text = "id: ${chosenFormat.format_id}"
val codec = val codec =
if (chosenFormat.encoding != "") { if (chosenFormat.encoding != "") {

View file

@ -221,10 +221,13 @@ class DownloadWorker(
request.addOption("--embed-chapters") request.addOption("--embed-chapters")
} }
if (downloadItem.videoPreferences.embedSubs) { if (downloadItem.videoPreferences.embedSubs) {
request.addOption("--embed-subs", "") request.addOption("--embed-subs")
request.addOption("--sub-langs", "en.*,.*-orig")
} }
val defaultFormats = context.resources.getStringArray(R.array.video_formats) val defaultFormats = context.resources.getStringArray(R.array.video_formats)
if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty()) request.addOption("--audio-multistreams")
var videoFormatID = downloadItem.format.format_id var videoFormatID = downloadItem.format.format_id
Log.e(TAG, videoFormatID) Log.e(TAG, videoFormatID)
var formatArgument = if (downloadItem.videoPreferences.removeAudio) "bestvideo" else "bestvideo+bestaudio/best" var formatArgument = if (downloadItem.videoPreferences.removeAudio) "bestvideo" else "bestvideo+bestaudio/best"
@ -232,13 +235,19 @@ class DownloadWorker(
if (videoFormatID == context.resources.getString(R.string.best_quality) || videoFormatID == "best") videoFormatID = "bestvideo" if (videoFormatID == context.resources.getString(R.string.best_quality) || videoFormatID == "best") videoFormatID = "bestvideo"
else if (videoFormatID == context.resources.getString(R.string.worst_quality) || videoFormatID == "worst") videoFormatID = "worst" else if (videoFormatID == context.resources.getString(R.string.worst_quality) || videoFormatID == "worst") videoFormatID = "worst"
else if (defaultFormats.contains(videoFormatID)) videoFormatID = "bestvideo[height<="+videoFormatID.substring(0, videoFormatID.length -1)+"]" else if (defaultFormats.contains(videoFormatID)) videoFormatID = "bestvideo[height<="+videoFormatID.substring(0, videoFormatID.length -1)+"]"
if (!downloadItem.videoPreferences.removeAudio) formatArgument = "$videoFormatID+bestaudio/best/$videoFormatID"
formatArgument = if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty()){
val audioIds = downloadItem.videoPreferences.audioFormatIDs.joinToString("+")
"$videoFormatID+$audioIds/best/$videoFormatID"
}else{
"$videoFormatID+bestaudio/best/$videoFormatID"
}
} }
Log.e(TAG, formatArgument) Log.e(TAG, formatArgument)
request.addOption("-f", formatArgument) request.addOption("-f", formatArgument)
val format = downloadItem.format.container val format = downloadItem.format.container
if(format.isNotEmpty() && format != "Default" && format != context.getString(R.string.defaultValue)){ if(format.isNotEmpty() && format != "Default" && format != context.getString(R.string.defaultValue)){
request.addOption("--merge-output-format", format) request.addOption("--merge-output-format", format.lowercase())
if (format != "webm") { if (format != "webm") {
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
if (embedThumb) { if (embedThumb) {
@ -324,6 +333,9 @@ class DownloadWorker(
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}, 1000) }, 1000)
} }
val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao, false, notificationUtil)
//put download in history //put download in history
val incognito = sharedPreferences.getBoolean("incognito", false) val incognito = sharedPreferences.getBoolean("incognito", false)
if (!incognito) { if (!incognito) {
@ -342,7 +354,7 @@ class DownloadWorker(
NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID
) )
if (updateDownloadItem(downloadItem, infoUtil, dao, resultDao, false, notificationUtil)){ if (wasQuickDownloaded){
runCatching { runCatching {
setProgressAsync(workDataOf("progress" to 100, "output" to "Creating Result Items", "id" to downloadItem.id, "log" to false)) setProgressAsync(workDataOf("progress" to 100, "output" to "Creating Result Items", "id" to downloadItem.id, "log" to false))
runBlocking { runBlocking {

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

View file

@ -187,7 +187,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
app:backgroundTint="?attr/colorSurface" app:backgroundTint="?attr/colorSurface"
app:cornerRadius="10dp" app:cornerRadius="15dp"
app:icon="@drawable/ic_cancel" app:icon="@drawable/ic_cancel"
app:iconSize="30dp" app:iconSize="30dp"
app:iconTint="?android:textColorPrimary" app:iconTint="?android:textColorPrimary"
@ -199,7 +199,7 @@
<TextView <TextView
android:id="@+id/output" android:id="@+id/output"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp" android:layout_height="40dp"
android:clickable="true" android:clickable="true"
android:ellipsize="end" android:ellipsize="end"
android:focusable="true" android:focusable="true"

View file

@ -1,113 +1,136 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto" <com.google.android.material.card.MaterialCardView xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/format_card_constraintLayout" android:id="@+id/format_card_constraintLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:clickable="true"
android:layout_height="wrap_content"
android:paddingVertical="5dp"
android:paddingHorizontal="10dp"
android:background="?android:attr/selectableItemBackground"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:focusable="true"> android:checkable="true"
android:clickable="true"
<TextView android:focusable="true"
android:id="@+id/container" android:backgroundTint="@android:color/transparent"
android:layout_width="60dp" app:checkedIcon="@null"
android:background="@drawable/rounded_corner" app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
android:backgroundTint="?attr/colorPrimaryInverse" app:strokeWidth="0dp"
android:textColor="@color/white" app:cardPreventCornerOverlap="true"
android:clickable="false" android:layout_height="wrap_content">
android:layout_height="55dp"
android:gravity="center"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="0dp" android:paddingVertical="5dp"
android:orientation="vertical" android:paddingHorizontal="10dp"
app:layout_constraintBottom_toBottomOf="parent" android:layout_height="wrap_content">
app:layout_constraintStart_toEndOf="@+id/container"
app:layout_constraintTop_toTopOf="parent">
<TextView <TextView
android:id="@+id/format_note" android:id="@+id/container"
android:layout_width="wrap_content" android:layout_width="60dp"
android:layout_height="wrap_content" android:background="@drawable/rounded_corner"
android:layout_marginStart="10dp" android:backgroundTint="?attr/colorPrimaryInverse"
android:textSize="20sp" android:textColor="@color/white"
android:clickable="false"
android:layout_height="55dp"
android:gravity="center"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom" android:orientation="vertical"
android:layout_marginStart="10dp"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@+id/format_note"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/container"
app:layout_constraintTop_toTopOf="parent">
<TextView <TextView
android:id="@+id/codec" android:id="@+id/format_note"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
app:layout_constraintHorizontal_bias="0.0"
android:maxLines="2"
android:ellipsize="end" android:ellipsize="end"
android:maxLength="17" android:textSize="20sp"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary" app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:textStyle="bold"
android:paddingHorizontal="5dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginStart="10dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/format_note"
app:layout_constraintVertical_bias="1.0">
<TextView
android:id="@+id/codec"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Tertiary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
<TextView <TextView
android:id="@+id/file_size" android:id="@+id/format_id"
style="@style/Widget.Material3.FloatingActionButton.Large.Tertiary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false" android:clickable="false"
android:gravity="center" android:gravity="bottom|end"
android:maxLength="10"
android:minWidth="30dp" android:minWidth="30dp"
android:textStyle="bold"
android:paddingHorizontal="5dp"
app:cornerRadius="10dp" app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/linearLayout"
app:layout_constraintTop_toBottomOf="@+id/format_note" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/format_id"
android:layout_width="wrap_content"
android:layout_height="55dp"
android:clickable="false"
android:gravity="bottom|end"
android:maxLength="10"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </com.google.android.material.card.MaterialCardView>

View file

@ -3,13 +3,14 @@
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"
android:orientation="vertical" android:orientation="vertical"
android:layout_height="match_parent"> android:layout_height="wrap_content">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="20dp" android:paddingBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
@ -40,6 +41,18 @@
app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_title" /> app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_title" />
<Button
android:id="@+id/format_ok"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/ok"
app:icon="@drawable/ic_check"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button <Button
android:id="@+id/format_refresh" android:id="@+id/format_refresh"
style="@style/Widget.Material3.Button.ElevatedButton.Icon" style="@style/Widget.Material3.Button.ElevatedButton.Icon"
@ -57,9 +70,9 @@
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:scrollbars="none" android:layout_height="wrap_content"
android:paddingTop="10dp" android:paddingTop="10dp"
android:layout_height="wrap_content"> android:scrollbars="none">
<LinearLayout <LinearLayout
android:id="@+id/format_list_linear_layout" android:id="@+id/format_list_linear_layout"
@ -67,6 +80,46 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout
android:id="@+id/video_linear_layout_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/video_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:text="@string/video" />
<LinearLayout
android:id="@+id/video_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
<LinearLayout
android:id="@+id/audio_linear_layout_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/audio_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:text="@string/audio" />
<LinearLayout
android:id="@+id/audio_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
@ -75,10 +128,10 @@
<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:orientation="vertical" android:orientation="vertical"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:paddingTop="10dp" android:paddingTop="10dp">
android:layout_height="match_parent">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -101,5 +154,4 @@
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,22 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity" >
<item
android:id="@+id/delete_results"
android:title="@string/remove_results"
android:icon="@drawable/baseline_delete_24"
app:showAsAction="ifRoom" />
<item
android:id="@+id/select_all"
android:title="@string/select_all"
app:showAsAction="never" />
<item
android:id="@+id/invert_selected"
android:title="@string/invert_selected"
app:showAsAction="never" />
</menu>

View file

@ -133,12 +133,12 @@
</array> </array>
<array name="formats_source"> <array name="formats_source">
<item>Invidious API [Youtube]</item> <item>PIPED [Youtube]</item>
<item>yt-dlp</item> <item>yt-dlp</item>
</array> </array>
<array name="formats_source_values"> <array name="formats_source_values">
<item>invidious</item> <item>piped</item>
<item>yt-dlp</item> <item>yt-dlp</item>
</array> </array>

View file

@ -262,4 +262,7 @@
<string name="backup_restore">Backup And Restore</string> <string name="backup_restore">Backup And Restore</string>
<string name="socks5_proxy_summary">Use the specified HTTP/HTTPS/SOCKS proxy</string> <string name="socks5_proxy_summary">Use the specified HTTP/HTTPS/SOCKS proxy</string>
<string name="socks5_proxy">Socks5 Proxy URL</string> <string name="socks5_proxy">Socks5 Proxy URL</string>
<string name="search_suggestions">Search Suggestions</string>
<string name="search_suggestions_summary">Get search suggestions from google</string>
<string name="preferred_format_id_summary">Selec the format with this ID in the download card</string>
</resources> </resources>

View file

@ -64,6 +64,14 @@
app:summary="@string/preferred_search_engine_summary" app:summary="@string/preferred_search_engine_summary"
app:title="@string/preferred_search_engine" /> app:title="@string/preferred_search_engine" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_search_accent"
android:key="search_suggestions"
app:summary="@string/search_suggestions_summary"
app:title="@string/search_suggestions" />
<ListPreference <ListPreference
android:defaultValue="" android:defaultValue=""
android:entries="@array/countries" android:entries="@array/countries"

View file

@ -122,6 +122,6 @@
<EditTextPreference <EditTextPreference
app:key="format_id" app:key="format_id"
app:defaultValue="" app:defaultValue=""
app:useSimpleSummaryProvider="true" app:summary="@string/preferred_format_id_summary"
app:title="@string/preferred_format_id" /> app:title="@string/preferred_format_id" />
</PreferenceScreen> </PreferenceScreen>

View file

@ -80,6 +80,11 @@
android:data="https://github.com/deniscerri/ytdlnis" /> android:data="https://github.com/deniscerri/ytdlnis" />
</Preference> </Preference>
<Preference
app:icon="@drawable/ic_info"
app:key="version"
app:title="@string/version"/>
</PreferenceCategory> </PreferenceCategory>

View file

@ -45,8 +45,4 @@
android:key="update_app" android:key="update_app"
app:summary="@string/update_app_summary" app:summary="@string/update_app_summary"
app:title="@string/update_app" /> app:title="@string/update_app" />
<Preference
app:icon="@drawable/ic_info"
app:key="version"
app:title="@string/version"/>
</PreferenceScreen> </PreferenceScreen>