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 {
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")
fun getDownloadById(id: Long) : DownloadItem
@Query("SELECT id FROM downloads ORDER BY id DESC LIMIT 1")
fun getLastDownloadId(): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(item: DownloadItem) : Long

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
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.VideoPreferences
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.work.DownloadWorker
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
@ -39,6 +43,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
private val repository : DownloadRepository
private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao
private val infoUtil : InfoUtil
val allDownloads : LiveData<List<DownloadItem>>
val queuedDownloads : LiveData<List<DownloadItem>>
val activeDownloads : LiveData<List<DownloadItem>>
@ -62,6 +67,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository = DownloadRepository(dao)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
infoUtil = InfoUtil(application)
allDownloads = repository.allDownloads.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()
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")
if (videoContainer == "Default") videoContainer = application.getString(R.string.defaultValue)
if (videoContainer == "Default") videoContainer = App.instance.getString(R.string.defaultValue)
defaultVideoFormats = mutableListOf()
videoFormat.forEach {
@ -95,7 +101,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
bestVideoFormat = defaultVideoFormats.last()
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(
getApplication<App>().resources.getString(R.string.best_quality),
audioContainer!!,
@ -112,7 +118,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun updateDownload(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){
repository.update(item);
repository.update(item)
}
fun getItemByID(id: Long) : DownloadItem {
@ -317,20 +323,20 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
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>()
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)) }
return formats
}
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>()
var containerPreference = sharedPreferences.getString("video_format", "Default")
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
}
@ -385,14 +391,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return repository.getActiveAndQueuedDownloads()
}
suspend fun queueDownloads(items: List<DownloadItem>) {
val context = getApplication<App>().applicationContext
val activeAndQueuedDownloads = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloads()
}
suspend fun queueDownloads(items: List<DownloadItem>) = CoroutineScope(Dispatchers.IO).launch {
val context = App.instance
val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads()
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val queuedItems = mutableListOf<DownloadItem>()
var lastDownloadId = repository.getLastDownloadId()
items.forEach {
lastDownloadId++
it.status = DownloadRepository.Status.Queued.toString()
if (activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
@ -401,12 +407,32 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} != null) {
Toast.makeText(context, context.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
}else{
it.id = withContext(Dispatchers.IO){
repository.insert(it)
}
it.id = lastDownloadId
val insert = async {repository.insert(it)}
val id = insert.await()
it.id = id
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 {
val currentTime = System.currentTimeMillis()
var delay = if (it.downloadStartTime != 0L){

View file

@ -376,13 +376,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}else{
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_plus, 0)
}
val suggestions = withContext(Dispatchers.IO){
if (it!!.isEmpty()) {
resultViewModel.getSearchHistory().map { it.query }
}else{
infoUtil!!.getSearchSuggestions(it.toString())
val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){
withContext(Dispatchers.IO){
if (it!!.isEmpty()) {
resultViewModel.getSearchHistory().map { it.query }
}else{
infoUtil!!.getSearchSuggestions(it.toString())
}
}
}
} else emptyList()
if (it!!.isEmpty()){
for (i in suggestions.indices) {
@ -445,8 +447,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}
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.util.FileUtil
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.dialog.MaterialAlertDialogBuilder
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()
val formatCard = view.findViewById<ConstraintLayout>(R.id.format_card_constraintLayout)
val formatCard = view.findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
val chosenFormat = downloadItem.format
uiUtil.populateFormatCard(formatCard, chosenFormat)
uiUtil.populateFormatCard(formatCard, chosenFormat, null)
val listener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, item: List<Format>) {
downloadItem.format = item.first()
uiUtil.populateFormatCard(formatCard, item.first())
uiUtil.populateFormatCard(formatCard, item.first(), null)
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultItem.formats.removeAll(formats.toSet())
@ -156,6 +157,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
}
}
formats = allFormats.first().toMutableList()
downloadItem.format.container = container.editText?.text.toString()
}
}
formatCard.setOnClickListener{

View file

@ -7,6 +7,7 @@ import android.content.Intent
import android.os.Bundle
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater
import android.view.View
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.receiver.ShareActivity
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
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 commandTemplateDao : CommandTemplateDao
private lateinit var uiUtil: UiUtil
private lateinit var infoUtil: InfoUtil
private lateinit var downloadAudioFragment: DownloadAudioFragment
private lateinit var downloadVideoFragment: DownloadVideoFragment
@ -58,6 +61,7 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao
uiUtil = UiUtil(FileUtil())
infoUtil = InfoUtil(requireContext())
}
@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.util.FileUtil
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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
@ -133,8 +134,8 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
))
var formats = mutableListOf<Format>()
formats.addAll(resultItem.formats.filter { !it.format_note.contains("audio", ignoreCase = true) })
if (formats.isEmpty()) formats.addAll(downloadItem.allFormats.filter { !it.format_note.contains("audio", ignoreCase = true) })
formats.addAll(resultItem.formats)
if (formats.isEmpty()) formats.addAll(downloadItem.allFormats)
val containers = requireContext().resources.getStringArray(R.array.video_containers)
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()
val formatCard = view.findViewById<ConstraintLayout>(R.id.format_card_constraintLayout)
val formatCard = view.findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
val chosenFormat = downloadItem.format
uiUtil.populateFormatCard(formatCard, chosenFormat)
uiUtil.populateFormatCard(formatCard, chosenFormat, null)
val listener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, item: List<Format>) {
downloadItem.format = item.first()
downloadItem.videoPreferences.audioFormatIDs.addAll(item.drop(1).map { it.format_id })
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultItem.formats.removeAll(formats.toSet())
@ -160,7 +162,8 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
}
}
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{

View file

@ -8,7 +8,7 @@ import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
@ -22,6 +22,8 @@ import com.deniscerri.ytdlnis.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior
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.launch
import kotlinx.coroutines.withContext
@ -36,6 +38,14 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private lateinit var sharedPreferences: SharedPreferences
private lateinit var formatCollection: MutableList<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?) {
super.onCreate(savedInstanceState)
@ -44,6 +54,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
infoUtil = InfoUtil(requireActivity().applicationContext)
formatCollection = mutableListOf()
chosenFormats = listOf()
selectedAudios = mutableListOf()
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
}
@ -61,8 +72,15 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
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)
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
val hasGenericFormats = when(items.first()!!.type){
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{
chosenFormats = formats.flatten()
}
addFormatsToView(linearLayout)
addFormatsToView()
}else{
chosenFormats = formats.flatten()
if(!hasGenericFormats){
chosenFormats = when(items.first()?.type){
Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
else -> chosenFormats.filter { !it.format_note.contains("audio", ignoreCase = true) }
if(items.first()?.type == Type.audio){
chosenFormats = chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
}
}
addFormatsToView(linearLayout)
addFormatsToView()
}
val refreshBtn = view.findViewById<Button>(R.id.format_refresh)
@ -107,7 +124,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
lifecycleScope.launch {
try {
refreshBtn.isEnabled = false
linearLayout.visibility = View.GONE
formatListLinearLayout.visibility = View.GONE
shimmers.visibility = View.VISIBLE
shimmers.startShimmer()
@ -117,9 +134,8 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
infoUtil.getFormats(items.first()!!.url)
}
chosenFormats = res.formats.filter { it.filesize != 0L }
chosenFormats = when(items.first()?.type){
Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
else -> chosenFormats.filter { !it.format_note.contains("audio", ignoreCase = true) }
if(items.first()?.type == Type.audio){
chosenFormats = chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
}
if (chosenFormats.isEmpty()) throw Exception()
//playlist format filtering
@ -150,18 +166,16 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
.sumOf { itt -> itt.filesize }
}
}
addFormatsToView(linearLayout)
refreshBtn.visibility = View.GONE
linearLayout.visibility = View.VISIBLE
shimmers.visibility = View.GONE
shimmers.stopShimmer()
addFormatsToView()
refreshBtn.visibility = View.GONE
formatListLinearLayout.visibility = View.VISIBLE
}catch (e: Exception){
runCatching {
refreshBtn.isEnabled = true
refreshBtn.text = getString(R.string.update_formats)
linearLayout.visibility = View.VISIBLE
formatListLinearLayout.visibility = View.VISIBLE
shimmers.visibility = View.GONE
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){
refreshBtn.performClick()
}
}
private fun addFormatsToView(linearLayout: LinearLayout){
linearLayout.removeAllViews()
private fun addFormatsToView(){
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){
val format = chosenFormats[i]
val formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null)
uiUtil.populateFormatCard(formatItem as ConstraintLayout, format)
formatItem.setOnClickListener{_ ->
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)
formatItem.tag = "${format.format_id}${format.format_note}"
uiUtil.populateFormatCard(formatItem as MaterialCardView, format, null)
formatItem.setOnClickListener{ clickedformat ->
//if the context is behind a single download card and its a video, allow the ability to multiselect audio formats
if (isSingleAndVideo){
val clickedCard = (clickedformat as MaterialCardView)
if (format.vcodec.isNotBlank() && format.vcodec != "none") {
videoFormatList.forEach { (it as MaterialCardView).isChecked = false }
selectedVideo = format
clickedCard.isChecked = true
}else{
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 {
uiUtil.showFormatDetails(format, requireActivity())
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.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
@ -55,6 +59,8 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
private lateinit var queuedRecyclerView : RecyclerView
private lateinit var queuedDownloads : GenericDownloadAdapter
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 fileUtil: FileUtil
private lateinit var uiUtil: UiUtil
@ -70,6 +76,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
notificationUtil = NotificationUtil(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
items = mutableListOf()
selectedObjects = arrayListOf()
fileUtil = FileUtil()
uiUtil = UiUtil(fileUtil)
return fragmentView
@ -226,7 +233,23 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
}
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){
@ -251,6 +274,76 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
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 =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
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 cookieViewModel: CookieViewModel
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private var version: Preference? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
val navController = findNavController()
@ -192,6 +195,24 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
appRestoreResultLauncher.launch(intent)
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 {

View file

@ -18,7 +18,6 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.updating
private var updateYTDL: Preference? = null
private var ytdlVersion: Preference? = null
private var version: Preference? = null
private var updateUtil: UpdateUtil? = null
@ -30,8 +29,6 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
val editor = preferences.edit()
updateYTDL = findPreference("update_ytdl")
ytdlVersion = findPreference("ytdl-version")
version = findPreference("version")
version!!.summary = BuildConfig.VERSION_NAME
YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it)
@ -69,20 +66,6 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
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.database.models.Format
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.google.common.net.HttpHeaders.USER_AGENT
import com.google.gson.Gson
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.BufferedReader
import java.io.File
import java.io.IOException
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.util.*
import java.util.regex.Pattern
class InfoUtil(private val context: Context) {
private var items: ArrayList<ResultItem?>
private lateinit var sharedPreferences: SharedPreferences
@ -42,16 +48,10 @@ class InfoUtil(private val context: Context) {
private fun init(){
if (countryCODE.isEmpty()){
val country = genericRequest("https://ipwho.is/")
countryCODE = try {
country.getString("country_code")
} catch (e: Exception) {
"US"
}
countryCODE = "US"
}
if (!useInvidous){
val res = genericRequest(invidousURL + "stats")
Log.e("Assa", res.toString())
useInvidous = res.length() != 0
}
}
@ -235,14 +235,10 @@ class InfoUtil(private val context: Context) {
try {
init()
if (key!!.isEmpty()) {
return if (useInvidous) {
val res = genericRequest(invidousURL + "videos/" + id)
if (res.length() == 0) getFromYTDL("https://www.youtube.com/watch?v=$id")[0] else createVideoFromInvidiousJSON(
res
)
} else {
getFromYTDL("https://www.youtube.com/watch?v=$id")[0]
}
val res = genericRequest(pipedURL + "streams/" + id)
return if (res.length() == 0) getFromYTDL("https://www.youtube.com/watch?v=$id")[0] else createVideoFromPipedJSON(
res, id
)
}
//short data
@ -345,6 +341,82 @@ class InfoUtil(private val context: Context) {
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 {
Log.e(TAG, url)
val reader: BufferedReader
@ -507,12 +579,12 @@ class InfoUtil(private val context: Context) {
val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?")
val m = p.matcher(url)
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 res = genericRequest(invidousURL + "videos/" + id)
val res = genericRequest(pipedURL + "streams/" + id)
if (res.length() == 0) getFromYTDL(url)[0]!!
else createVideoFromInvidiousJSON(
res
else createVideoFromPipedJSON(
res, id
)!!
}else{
getFromYTDL(url)[0]!!
@ -583,8 +655,8 @@ class InfoUtil(private val context: Context) {
}else{
urls.forEach {
val id = getIDFromYoutubeURL(it)
val res = genericRequest(invidousURL + "videos/" + id)
val vid = createVideoFromInvidiousJSON(res)
val res = genericRequest(pipedURL + "streams/" + id)
val vid = createVideoFromPipedJSON(res, id)
progress(vid!!.formats)
}
}
@ -881,6 +953,7 @@ class InfoUtil(private val context: Context) {
companion object {
private const val TAG = "API MANAGER"
private const val invidousURL = "https://invidious.nerdvpn.de/api/v1/"
private const val pipedURL = "https://pipedapi.kavin.rocks/"
private var countryCODE: String = ""
}
}

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.util
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
@ -30,6 +31,7 @@ import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.google.android.material.bottomsheet.BottomSheetBehavior
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.ChipGroup
import com.google.android.material.datepicker.CalendarConstraints
@ -44,9 +46,15 @@ import java.io.File
import java.util.Calendar
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.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}"
val codec =
if (chosenFormat.encoding != "") {

View file

@ -221,10 +221,13 @@ class DownloadWorker(
request.addOption("--embed-chapters")
}
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)
if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty()) request.addOption("--audio-multistreams")
var videoFormatID = downloadItem.format.format_id
Log.e(TAG, videoFormatID)
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"
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)+"]"
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)
request.addOption("-f", formatArgument)
val format = downloadItem.format.container
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") {
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
if (embedThumb) {
@ -324,6 +333,9 @@ class DownloadWorker(
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}, 1000)
}
val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao, false, notificationUtil)
//put download in history
val incognito = sharedPreferences.getBoolean("incognito", false)
if (!incognito) {
@ -342,7 +354,7 @@ class DownloadWorker(
NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID
)
if (updateDownloadItem(downloadItem, infoUtil, dao, resultDao, false, notificationUtil)){
if (wasQuickDownloaded){
runCatching {
setProgressAsync(workDataOf("progress" to 100, "output" to "Creating Result Items", "id" to downloadItem.id, "log" to false))
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_marginEnd="10dp"
app:backgroundTint="?attr/colorSurface"
app:cornerRadius="10dp"
app:cornerRadius="15dp"
app:icon="@drawable/ic_cancel"
app:iconSize="30dp"
app:iconTint="?android:textColorPrimary"
@ -199,7 +199,7 @@
<TextView
android:id="@+id/output"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_height="40dp"
android:clickable="true"
android:ellipsize="end"
android:focusable="true"

View file

@ -1,113 +1,136 @@
<?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: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"
android:focusable="true">
<TextView
android:id="@+id/container"
android:layout_width="60dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorPrimaryInverse"
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_constraintTop_toTopOf="parent" />
android:checkable="true"
android:clickable="true"
android:focusable="true"
android:backgroundTint="@android:color/transparent"
app:checkedIcon="@null"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
app:strokeWidth="0dp"
app:cardPreventCornerOverlap="true"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/container"
app:layout_constraintTop_toTopOf="parent">
android:layout_width="match_parent"
android:paddingVertical="5dp"
android:paddingHorizontal="10dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/format_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:textSize="20sp"
android:id="@+id/container"
android:layout_width="60dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorPrimaryInverse"
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_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="wrap_content"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginStart="10dp"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@+id/format_note"
android:orientation="vertical"
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
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:maxLength="17"
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:gravity="center"
android:minWidth="30dp"
android:textStyle="bold"
android:paddingHorizontal="5dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="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
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Tertiary"
android:id="@+id/format_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:layout_height="0dp"
android:clickable="false"
android:gravity="center"
android:gravity="bottom|end"
android:maxLength="10"
android:minWidth="30dp"
android:textStyle="bold"
android:paddingHorizontal="5dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintEnd_toEndOf="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>
<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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_height="match_parent">
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="20dp"
android:paddingBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent">
@ -40,6 +41,18 @@
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
android:id="@+id/format_refresh"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
@ -57,9 +70,9 @@
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:scrollbars="none"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:layout_height="wrap_content">
android:scrollbars="none">
<LinearLayout
android:id="@+id/format_list_linear_layout"
@ -67,6 +80,46 @@
android:layout_height="wrap_content"
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>
</androidx.core.widget.NestedScrollView>
@ -75,10 +128,10 @@
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/format_list_shimmer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingHorizontal="10dp"
android:paddingTop="10dp"
android:layout_height="match_parent">
android:paddingTop="10dp">
<LinearLayout
android:layout_width="match_parent"
@ -101,5 +154,4 @@
</LinearLayout>
</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 name="formats_source">
<item>Invidious API [Youtube]</item>
<item>PIPED [Youtube]</item>
<item>yt-dlp</item>
</array>
<array name="formats_source_values">
<item>invidious</item>
<item>piped</item>
<item>yt-dlp</item>
</array>

View file

@ -262,4 +262,7 @@
<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">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>

View file

@ -64,6 +64,14 @@
app:summary="@string/preferred_search_engine_summary"
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
android:defaultValue=""
android:entries="@array/countries"

View file

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

View file

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

View file

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