stuff
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:
parent
35f8f939e8
commit
aadac439f3
28 changed files with 630 additions and 211 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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? = "",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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){
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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("'s", "'")
|
||||
title.replace("&", "&")
|
||||
val author = obj.getString("uploader").toString()
|
||||
author.replace("'s", "'")
|
||||
author.replace("&", "&")
|
||||
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
|
|
@ -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 != "") {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
5
app/src/main/res/drawable/ic_search_accent.xml
Normal file
5
app/src/main/res/drawable/ic_search_accent.xml
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
22
app/src/main/res/menu/queued_menu_context.xml
Normal file
22
app/src/main/res/menu/queued_menu_context.xml
Normal 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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue