more stuff
Quick download item log has no title now, maybe you can use "null" or the link? 'Cause it looks hilarious, LOL Fix search history not showing sometimes Add download type in logs fix video not showing in cut screen fixed xhamster not showing results
This commit is contained in:
parent
366af9270e
commit
f303a4f879
32 changed files with 1265 additions and 439 deletions
|
|
@ -192,8 +192,18 @@ dependencies {
|
|||
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.10"
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
|
||||
implementation "com.google.android.exoplayer:exoplayer:2.18.7"
|
||||
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
|
||||
implementation "com.github.Irineu333:Highlight-KT:1.0.4"
|
||||
|
||||
// For media playback using ExoPlayer
|
||||
implementation("androidx.media3:media3-exoplayer:$media3_version")
|
||||
// For DASH playback support with ExoPlayer
|
||||
implementation("androidx.media3:media3-exoplayer-dash:$media3_version")
|
||||
// For HLS playback support with ExoPlayer
|
||||
implementation("androidx.media3:media3-exoplayer-hls:$media3_version")
|
||||
implementation("androidx.media3:media3-ui:$media3_version")
|
||||
// For RTSP playback support with ExoPlayer
|
||||
implementation("androidx.media3:media3-exoplayer-rtsp:$media3_version")
|
||||
implementation("androidx.media3:media3-datasource-cronet:$media3_version")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "b21f2aa737d29eef2c1bf6418b314726",
|
||||
"identityHash": "3d472df655311f8e531602dca2886c34",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "results",
|
||||
|
|
@ -180,7 +180,7 @@
|
|||
},
|
||||
{
|
||||
"tableName": "downloads",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `thumb` TEXT NOT NULL, `duration` TEXT NOT NULL, `type` TEXT NOT NULL, `format` TEXT NOT NULL, `container` TEXT NOT NULL DEFAULT 'Default', `downloadSections` TEXT NOT NULL DEFAULT '', `allFormats` TEXT NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `downloadSize` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `audioPreferences` TEXT NOT NULL, `videoPreferences` TEXT NOT NULL, `customFileNameTemplate` TEXT NOT NULL, `SaveThumb` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued', `downloadStartTime` INTEGER NOT NULL DEFAULT 0, `logID` INTEGER)",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `thumb` TEXT NOT NULL, `duration` TEXT NOT NULL, `type` TEXT NOT NULL, `format` TEXT NOT NULL, `container` TEXT NOT NULL DEFAULT 'Default', `downloadSections` TEXT NOT NULL DEFAULT '', `allFormats` TEXT NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `downloadSize` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `audioPreferences` TEXT NOT NULL, `videoPreferences` TEXT NOT NULL, `extraCommands` TEXT NOT NULL DEFAULT '', `customFileNameTemplate` TEXT NOT NULL, `SaveThumb` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued', `downloadStartTime` INTEGER NOT NULL DEFAULT 0, `logID` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
|
|
@ -286,6 +286,13 @@
|
|||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extraCommands",
|
||||
"columnName": "extraCommands",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "customFileNameTemplate",
|
||||
"columnName": "customFileNameTemplate",
|
||||
|
|
@ -446,7 +453,7 @@
|
|||
},
|
||||
{
|
||||
"tableName": "logs",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL)",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadType` TEXT NOT NULL, `downloadTime` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
|
|
@ -465,6 +472,24 @@
|
|||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "format",
|
||||
"columnName": "format",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "downloadType",
|
||||
"columnName": "downloadType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "downloadTime",
|
||||
"columnName": "downloadTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
|
|
@ -480,7 +505,7 @@
|
|||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b21f2aa737d29eef2c1bf6418b314726')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3d472df655311f8e531602dca2886c34')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -16,16 +16,20 @@ import android.os.Handler
|
|||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import android.widget.CheckBox
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.deniscerri.ytdlnis.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.text.format.DateFormat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
|
@ -12,29 +14,38 @@ import androidx.recyclerview.widget.DiffUtil
|
|||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.models.LogItem
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class DownloadLogsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<LogItem?, DownloadLogsAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) {
|
||||
private val onItemClickListener: OnItemClickListener
|
||||
private val activity: Activity
|
||||
private val checkedItems: ArrayList<Long>
|
||||
|
||||
init {
|
||||
checkedItems = ArrayList()
|
||||
this.onItemClickListener = onItemClickListener
|
||||
this.activity = activity
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
|
||||
val item: ConstraintLayout
|
||||
val item: MaterialCardView
|
||||
|
||||
init {
|
||||
item = itemView.findViewById(R.id.download_log_item_constraint)
|
||||
item = itemView.findViewById(R.id.log_card_view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val cardView = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.download_log_item, parent, false)
|
||||
.inflate(R.layout.log_card, parent, false)
|
||||
return ViewHolder(cardView, onItemClickListener)
|
||||
}
|
||||
|
||||
|
|
@ -45,20 +56,112 @@ class DownloadLogsAdapter(onItemClickListener: OnItemClickListener, activity: Ac
|
|||
val title = card.findViewById<TextView>(R.id.title)
|
||||
title.text = item?.title
|
||||
|
||||
val delete = card.findViewById<Button>(R.id.delete)
|
||||
val downloadedTime = card.findViewById<TextView>(R.id.downloadedTime)
|
||||
downloadedTime.text = SimpleDateFormat(
|
||||
DateFormat.getBestDateTimePattern(
|
||||
Locale.getDefault(), "ddMMMyyyy - HH:mm"), Locale.getDefault()).format(item!!.downloadTime)
|
||||
|
||||
delete.setOnClickListener {
|
||||
onItemClickListener.onDeleteClick(item!!)
|
||||
val downloadTypeIcon = card.findViewById<MaterialButton>(R.id.download_type)
|
||||
when(item.downloadType){
|
||||
DownloadViewModel.Type.audio -> downloadTypeIcon.setIconResource(R.drawable.ic_music)
|
||||
DownloadViewModel.Type.video -> downloadTypeIcon.setIconResource(R.drawable.ic_video)
|
||||
DownloadViewModel.Type.command -> downloadTypeIcon.setIconResource(R.drawable.ic_terminal)
|
||||
}
|
||||
|
||||
title.setOnClickListener {
|
||||
onItemClickListener.onItemClick(item!!)
|
||||
val formatNote = card.findViewById<TextView>(R.id.format_note)
|
||||
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
|
||||
View.GONE
|
||||
else formatNote!!.text = item.format.format_note.uppercase()
|
||||
|
||||
val codec = card.findViewById<TextView>(R.id.codec)
|
||||
val codecText =
|
||||
if (item.format.encoding != "") {
|
||||
item.format.encoding.uppercase()
|
||||
}else if (item.format.vcodec != "none" && item.format.vcodec != ""){
|
||||
item.format.vcodec.uppercase()
|
||||
} else {
|
||||
item.format.acodec.uppercase()
|
||||
}
|
||||
if (codecText == "" || codecText == "none"){
|
||||
codec.visibility = View.GONE
|
||||
}else{
|
||||
codec.visibility = View.VISIBLE
|
||||
codec.text = codecText
|
||||
}
|
||||
|
||||
val fileSize = card.findViewById<TextView>(R.id.file_size)
|
||||
val fileSizeReadable = FileUtil.convertFileSize(item.format.filesize)
|
||||
if (fileSizeReadable == "?") fileSize.visibility = View.GONE
|
||||
else fileSize.text = fileSizeReadable
|
||||
|
||||
if (checkedItems.contains(item.id)) {
|
||||
card.isChecked = true
|
||||
card.strokeWidth = 5
|
||||
} else {
|
||||
card.isChecked = false
|
||||
card.strokeWidth = 0
|
||||
}
|
||||
card.setOnClickListener {
|
||||
if (checkedItems.size > 0) {
|
||||
checkCard(card, item.id)
|
||||
} else {
|
||||
onItemClickListener.onItemClick(item.id)
|
||||
}
|
||||
}
|
||||
|
||||
card.setOnLongClickListener {
|
||||
checkCard(card, item.id)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun clearCheckeditems() {
|
||||
for (i in 0 until itemCount){
|
||||
val item = getItem(i)
|
||||
if (checkedItems.find { it == item?.id } != null){
|
||||
checkedItems.remove(item?.id)
|
||||
notifyItemChanged(i)
|
||||
}
|
||||
}
|
||||
|
||||
checkedItems.clear()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun checkAll(items: List<LogItem?>?){
|
||||
checkedItems.clear()
|
||||
checkedItems.addAll(items!!.map { it!!.id })
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun invertSelected(items: List<LogItem?>?){
|
||||
val invertedList = mutableListOf<Long>()
|
||||
items?.forEach {
|
||||
if (!checkedItems.contains(it!!.id)) invertedList.add(it.id)
|
||||
}
|
||||
checkedItems.clear()
|
||||
checkedItems.addAll(invertedList)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun checkCard(card: MaterialCardView, itemID: Long) {
|
||||
if (card.isChecked) {
|
||||
card.strokeWidth = 0
|
||||
checkedItems.remove(itemID)
|
||||
} else {
|
||||
card.strokeWidth = 5
|
||||
checkedItems.add(itemID)
|
||||
}
|
||||
card.isChecked = !card.isChecked
|
||||
onItemClickListener.onCardSelect(itemID, card.isChecked)
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(item: LogItem)
|
||||
fun onItemClick(itemID: Long)
|
||||
fun onDeleteClick(item: LogItem)
|
||||
fun onCardSelect(itemID: Long, isChecked: Boolean)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ data class DownloadItem(
|
|||
val playlistTitle: String,
|
||||
val audioPreferences : AudioPreferences,
|
||||
val videoPreferences: VideoPreferences,
|
||||
@ColumnInfo(defaultValue = "")
|
||||
var extraCommands: String,
|
||||
var customFileNameTemplate: String,
|
||||
var SaveThumb: Boolean,
|
||||
@ColumnInfo(defaultValue = "Queued")
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
package com.deniscerri.ytdlnis.database.models
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
|
||||
@Entity(tableName = "logs")
|
||||
data class LogItem(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long,
|
||||
var title: String,
|
||||
var content: String
|
||||
var content: String,
|
||||
var format: Format,
|
||||
var downloadType: DownloadViewModel.Type,
|
||||
var downloadTime: Long,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
sharedPreferences.getString("video_format", "Default")!!,
|
||||
"",
|
||||
resultItem.formats,
|
||||
downloadPath!!, resultItem.website, "", resultItem.playlistTitle, audioPreferences, videoPreferences,customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0, null
|
||||
downloadPath!!, resultItem.website, "", resultItem.playlistTitle, audioPreferences, videoPreferences, "", customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0, null
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -302,7 +302,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
|
|||
container,
|
||||
"",
|
||||
ArrayList(),
|
||||
path, historyItem.website, "", "", audioPreferences, videoPreferences,customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0, null
|
||||
path, historyItem.website, "", "", audioPreferences, videoPreferences, "", customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0, null
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import android.os.Bundle
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.util.ThemeUtil
|
||||
|
||||
|
|
|
|||
|
|
@ -239,7 +239,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
|
|||
arguments?.remove("url")
|
||||
}
|
||||
if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){
|
||||
updateSearchViewItems(searchView?.editText?.text, linkYouCopied)
|
||||
lifecycleScope.launch {
|
||||
updateSearchViewItems(searchView?.editText?.text, linkYouCopied)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -291,7 +293,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
|
|||
}else{
|
||||
linkYouCopied!!.visibility = GONE
|
||||
}
|
||||
updateSearchViewItems(searchView!!.editText.text, linkYouCopied)
|
||||
lifecycleScope.launch {
|
||||
updateSearchViewItems(searchView!!.editText.text, linkYouCopied)
|
||||
}
|
||||
}catch (e: Exception){
|
||||
e.printStackTrace()
|
||||
linkYouCopied!!.visibility = GONE
|
||||
|
|
@ -301,7 +305,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
|
|||
|
||||
searchView!!.editText.doAfterTextChanged {
|
||||
if (searchView!!.currentTransitionState != SearchView.TransitionState.SHOWN) return@doAfterTextChanged
|
||||
updateSearchViewItems(it, linkYouCopied)
|
||||
lifecycleScope.launch {
|
||||
updateSearchViewItems(it, linkYouCopied)
|
||||
}
|
||||
}
|
||||
|
||||
searchView!!.editText.setOnTouchListener(OnTouchListener { _, event ->
|
||||
|
|
@ -368,91 +374,89 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
|
|||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun updateSearchViewItems(it: Editable?, linkYouCopied: View?){
|
||||
lifecycleScope.launch {
|
||||
searchSuggestionsLinearLayout!!.removeAllViews()
|
||||
searchHistoryLinearLayout!!.removeAllViews()
|
||||
private suspend fun updateSearchViewItems(it: Editable?, linkYouCopied: View?){
|
||||
searchSuggestionsLinearLayout!!.removeAllViews()
|
||||
searchHistoryLinearLayout!!.removeAllViews()
|
||||
|
||||
searchSuggestionsLinearLayout!!.visibility = GONE
|
||||
searchHistoryLinearLayout!!.visibility = GONE
|
||||
linkYouCopied!!.visibility = GONE
|
||||
searchSuggestionsLinearLayout!!.visibility = GONE
|
||||
searchHistoryLinearLayout!!.visibility = GONE
|
||||
linkYouCopied!!.visibility = GONE
|
||||
|
||||
if (searchView!!.editText.text.isEmpty()){
|
||||
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
if (searchView!!.editText.text.isEmpty()){
|
||||
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}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 if (sharedPreferences!!.getBoolean("search_suggestions", false)){
|
||||
infoUtil!!.getSearchSuggestions(it.toString())
|
||||
}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 if (sharedPreferences!!.getBoolean("search_suggestions", false)){
|
||||
infoUtil!!.getSearchSuggestions(it.toString())
|
||||
}else{
|
||||
emptyList()
|
||||
}
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
if (it!!.isEmpty()){
|
||||
for (i in suggestions.indices) {
|
||||
val v = LayoutInflater.from(fragmentContext)
|
||||
.inflate(R.layout.search_suggestion_item, null)
|
||||
val textView = v.findViewById<TextView>(R.id.suggestion_text)
|
||||
textView.text = suggestions[i]
|
||||
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
searchHistoryLinearLayout!!.addView(
|
||||
v
|
||||
)
|
||||
}
|
||||
textView.setOnClickListener {
|
||||
searchView!!.setText(textView.text)
|
||||
initSearch(searchView!!)
|
||||
}
|
||||
textView.setOnLongClickListener {
|
||||
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + textView.text + "\"!")
|
||||
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
searchHistoryLinearLayout!!.removeView(v)
|
||||
resultViewModel.removeSearchQueryFromHistory(textView.text.toString())
|
||||
}
|
||||
deleteDialog.show()
|
||||
true
|
||||
if (it!!.isEmpty()){
|
||||
for (i in suggestions.indices) {
|
||||
val v = LayoutInflater.from(fragmentContext)
|
||||
.inflate(R.layout.search_suggestion_item, null)
|
||||
val textView = v.findViewById<TextView>(R.id.suggestion_text)
|
||||
textView.text = suggestions[i]
|
||||
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
searchHistoryLinearLayout!!.addView(
|
||||
v
|
||||
)
|
||||
}
|
||||
textView.setOnClickListener {
|
||||
searchView!!.setText(textView.text)
|
||||
initSearch(searchView!!)
|
||||
}
|
||||
textView.setOnLongClickListener {
|
||||
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + textView.text + "\"!")
|
||||
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
searchHistoryLinearLayout!!.removeView(v)
|
||||
resultViewModel.removeSearchQueryFromHistory(textView.text.toString())
|
||||
}
|
||||
deleteDialog.show()
|
||||
true
|
||||
}
|
||||
|
||||
val mb = v.findViewById<ImageButton>(R.id.set_search_query_button)
|
||||
mb.setOnClickListener {
|
||||
searchView!!.setText(textView.text)
|
||||
searchView!!.editText.setSelection(searchView!!.editText.length())
|
||||
}
|
||||
val mb = v.findViewById<ImageButton>(R.id.set_search_query_button)
|
||||
mb.setOnClickListener {
|
||||
searchView!!.setText(textView.text)
|
||||
searchView!!.editText.setSelection(searchView!!.editText.length())
|
||||
}
|
||||
searchHistoryLinearLayout!!.visibility = VISIBLE
|
||||
if (linkYouCopied.findViewById<TextView>(R.id.suggestion_text).text.isNotEmpty()){
|
||||
linkYouCopied.visibility = VISIBLE
|
||||
}
|
||||
}else{
|
||||
for (i in suggestions.indices) {
|
||||
val v = LayoutInflater.from(fragmentContext)
|
||||
.inflate(R.layout.search_suggestion_item, null)
|
||||
val textView = v.findViewById<TextView>(R.id.suggestion_text)
|
||||
textView.text = suggestions[i]
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
searchSuggestionsLinearLayout!!.addView(
|
||||
v
|
||||
)
|
||||
}
|
||||
textView.setOnClickListener {
|
||||
searchView!!.setText(textView.text)
|
||||
initSearch(searchView!!)
|
||||
}
|
||||
val mb = v.findViewById<ImageButton>(R.id.set_search_query_button)
|
||||
mb.setOnClickListener {
|
||||
searchView!!.setText(textView.text)
|
||||
searchView!!.editText.setSelection(searchView!!.editText.length())
|
||||
}
|
||||
}
|
||||
searchSuggestionsLinearLayout!!.visibility = VISIBLE
|
||||
}
|
||||
searchHistoryLinearLayout!!.visibility = VISIBLE
|
||||
if (linkYouCopied.findViewById<TextView>(R.id.suggestion_text).text.isNotEmpty()){
|
||||
linkYouCopied.visibility = VISIBLE
|
||||
}
|
||||
}else{
|
||||
for (i in suggestions.indices) {
|
||||
val v = LayoutInflater.from(fragmentContext)
|
||||
.inflate(R.layout.search_suggestion_item, null)
|
||||
val textView = v.findViewById<TextView>(R.id.suggestion_text)
|
||||
textView.text = suggestions[i]
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
searchSuggestionsLinearLayout!!.addView(
|
||||
v
|
||||
)
|
||||
}
|
||||
textView.setOnClickListener {
|
||||
searchView!!.setText(textView.text)
|
||||
initSearch(searchView!!)
|
||||
}
|
||||
val mb = v.findViewById<ImageButton>(R.id.set_search_query_button)
|
||||
mb.setOnClickListener {
|
||||
searchView!!.setText(textView.text)
|
||||
searchView!!.editText.setSelection(searchView!!.editText.length())
|
||||
}
|
||||
}
|
||||
searchSuggestionsLinearLayout!!.visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,26 +15,23 @@ import android.widget.*
|
|||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.children
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem.fromUri
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.cronet.CronetDataSource
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.MergingMediaSource
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.deniscerri.ytdlnis.App
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.models.ChapterItem
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.DefaultLoadControl
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.MediaItem.fromUri
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
||||
import com.google.android.exoplayer2.source.MediaSource
|
||||
import com.google.android.exoplayer2.source.MergingMediaSource
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
|
|
@ -53,15 +50,17 @@ import kotlinx.coroutines.flow.flow
|
|||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.chromium.net.CronetEngine
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
||||
class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls : String?, private var chapters: List<ChapterItem>?, private val listener: VideoCutListener) : BottomSheetDialogFragment() {
|
||||
private lateinit var behavior: BottomSheetBehavior<View>
|
||||
private lateinit var infoUtil: InfoUtil
|
||||
private lateinit var player: Player
|
||||
private lateinit var player: ExoPlayer
|
||||
|
||||
private lateinit var cutSection : ConstraintLayout
|
||||
private lateinit var durationText: TextView
|
||||
|
|
@ -92,6 +91,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
|
|||
|
||||
|
||||
@SuppressLint("RestrictedApi", "SetTextI18n")
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
override fun setupDialog(dialog: Dialog, style: Int) {
|
||||
super.setupDialog(dialog, style)
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.cut_video_sheet, null)
|
||||
|
|
@ -104,30 +104,53 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
|
|||
behavior.peekHeight = displayMetrics.heightPixels
|
||||
}
|
||||
|
||||
val renderersFactory = DefaultRenderersFactory(requireContext().applicationContext)
|
||||
.setEnableDecoderFallback(true)
|
||||
.setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER)
|
||||
.setMediaCodecSelector { mimeType, requiresSecureDecoder, requiresTunnelingDecoder ->
|
||||
var decoderInfo: MutableList<MediaCodecInfo> =
|
||||
MediaCodecSelector.DEFAULT
|
||||
.getDecoderInfos(
|
||||
mimeType,
|
||||
requiresSecureDecoder,
|
||||
requiresTunnelingDecoder
|
||||
)
|
||||
if (MimeTypes.VIDEO_H264 == mimeType) {
|
||||
// copy the list because MediaCodecSelector.DEFAULT returns an unmodifiable list
|
||||
decoderInfo = ArrayList(decoderInfo)
|
||||
decoderInfo.reverse()
|
||||
}
|
||||
decoderInfo
|
||||
}
|
||||
// val renderersFactory = DefaultRenderersFactory(requireContext().applicationContext)
|
||||
// .setEnableDecoderFallback(true)
|
||||
// .setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER)
|
||||
// .setMediaCodecSelector { mimeType, requiresSecureDecoder, requiresTunnelingDecoder ->
|
||||
// var decoderInfo: MutableList<MediaCodecInfo> =
|
||||
// MediaCodecSelector.DEFAULT
|
||||
// .getDecoderInfos(
|
||||
// mimeType,
|
||||
// requiresSecureDecoder,
|
||||
// requiresTunnelingDecoder
|
||||
// )
|
||||
// if (MimeTypes.VIDEO_H264 == mimeType) {
|
||||
// // copy the list because MediaCodecSelector.DEFAULT returns an unmodifiable list
|
||||
// decoderInfo = ArrayList(decoderInfo)
|
||||
// decoderInfo.reverse()
|
||||
// }
|
||||
// decoderInfo
|
||||
// }
|
||||
|
||||
val cronetEngine: CronetEngine = CronetEngine.Builder(App.instance)
|
||||
.enableHttp2(true)
|
||||
.enableQuic(true)
|
||||
.enableBrotli(true)
|
||||
.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, 1024L * 1024L) // 1MiB
|
||||
.build()
|
||||
|
||||
val trackSelector = DefaultTrackSelector(requireContext())
|
||||
val loadControl = DefaultLoadControl()
|
||||
val loadControl = DefaultLoadControl.Builder()
|
||||
// cache the last three minutes
|
||||
.setBackBuffer(1000 * 60 * 3, true)
|
||||
.setBufferDurationsMs(
|
||||
1000 * 10, // exo default is 50s
|
||||
50000,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||
)
|
||||
.build()
|
||||
|
||||
val cronetDataSourceFactory = CronetDataSource.Factory(
|
||||
cronetEngine,
|
||||
Executors.newCachedThreadPool()
|
||||
)
|
||||
val dataSourceFactory = DefaultDataSource.Factory(requireContext(), cronetDataSourceFactory)
|
||||
|
||||
player = ExoPlayer.Builder(requireContext())
|
||||
.setUsePlatformDiagnostics(false)
|
||||
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
|
||||
.setTrackSelector(trackSelector)
|
||||
.setLoadControl(loadControl)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
|
|
@ -139,8 +162,9 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
|
|||
,
|
||||
true)
|
||||
.build()
|
||||
|
||||
val frame = view.findViewById<MaterialCardView>(R.id.frame_layout)
|
||||
val videoView = view.findViewById<StyledPlayerView>(R.id.video_view)
|
||||
val videoView = view.findViewById<PlayerView>(R.id.video_view)
|
||||
videoView.player = player
|
||||
timeSeconds = convertStringToTimestamp(item.duration)
|
||||
if (chapters == null) chapters = emptyList()
|
||||
|
|
@ -199,7 +223,9 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
|
|||
val listType: Type = object : TypeToken<List<ChapterItem>>() {}.type
|
||||
chapters = Gson().fromJson(data.first().toString(), listType)
|
||||
data.removeFirst()
|
||||
}catch (ignored: Exception) {}
|
||||
}catch (ignored: Exception) {
|
||||
data.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
if (data.isEmpty()) throw Exception("No Streaming URL found!")
|
||||
|
|
@ -210,7 +236,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
|
|||
val videoSource: MediaSource =
|
||||
DefaultMediaSourceFactory(requireContext())
|
||||
.createMediaSource(fromUri(Uri.parse(data[1])))
|
||||
(player as ExoPlayer).setMediaSource(MergingMediaSource(videoSource, audioSource))
|
||||
player.setMediaSource(MergingMediaSource(videoSource, audioSource))
|
||||
}else{
|
||||
player.addMediaItem(fromUri(Uri.parse(data[0])))
|
||||
}
|
||||
|
|
@ -229,12 +255,12 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
|
|||
}
|
||||
//poll video progress
|
||||
lifecycleScope.launch {
|
||||
videoProgress(player).collect {
|
||||
val currentTime = infoUtil.formatIntegerDuration(it, Locale.US)
|
||||
videoProgress(player).collect { p ->
|
||||
val currentTime = infoUtil.formatIntegerDuration(p, Locale.US)
|
||||
durationText.text = "$currentTime / ${item.duration}"
|
||||
val startTimestamp = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
|
||||
val endTimestamp = convertStringToTimestamp(toTextInput.editText!!.text.toString())
|
||||
if (it >= endTimestamp){
|
||||
if (p >= endTimestamp){
|
||||
player.prepare()
|
||||
player.seekTo((startTimestamp * 1000).toLong())
|
||||
}
|
||||
|
|
@ -536,7 +562,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
|
|||
}
|
||||
|
||||
|
||||
private fun videoProgress(player: Player?) = flow {
|
||||
private fun videoProgress(player: ExoPlayer?) = flow {
|
||||
while (true) {
|
||||
emit((player!!.currentPosition / 1000).toInt())
|
||||
delay(1000)
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
package com.deniscerri.ytdlnis.ui.downloadcard
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
|
|
@ -33,7 +38,10 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
|
|||
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
|
||||
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
|
@ -41,6 +49,7 @@ import com.google.android.material.textfield.TextInputLayout
|
|||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
|
|
@ -56,6 +65,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
|
|||
private lateinit var author : TextInputLayout
|
||||
private lateinit var saveDir : TextInputLayout
|
||||
private lateinit var freeSpace : TextView
|
||||
private lateinit var infoUtil: InfoUtil
|
||||
|
||||
lateinit var downloadItem : DownloadItem
|
||||
override fun onCreateView(
|
||||
|
|
@ -68,7 +78,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
|
|||
activity = getActivity()
|
||||
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
|
||||
|
||||
infoUtil = InfoUtil(requireContext())
|
||||
return fragmentView
|
||||
}
|
||||
|
||||
|
|
@ -328,6 +338,47 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
|
|||
}else{
|
||||
cut.isEnabled = false
|
||||
}
|
||||
|
||||
|
||||
val extraCommands = view.findViewById<Chip>(R.id.extra_commands)
|
||||
if (sharedPreferences.getBoolean("use_extra_commands", false)){
|
||||
extraCommands.visibility = View.VISIBLE
|
||||
extraCommands.setOnClickListener {
|
||||
val bottomSheet = BottomSheetDialog(requireContext())
|
||||
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
bottomSheet.setContentView(R.layout.extra_commands_bottom_sheet)
|
||||
|
||||
val text = bottomSheet.findViewById<EditText>(R.id.command)
|
||||
val currentCommand = infoUtil.buildYoutubeDLRequest(downloadItem).buildCommand().joinToString(" ")
|
||||
bottomSheet.findViewById<TextView>(R.id.currentText)?.text = currentCommand
|
||||
|
||||
text?.setText(downloadItem.extraCommands)
|
||||
|
||||
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
text!!.postDelayed({
|
||||
text.setSelection(text.length())
|
||||
text.requestFocus()
|
||||
imm.showSoftInput(text, 0)
|
||||
}, 300)
|
||||
|
||||
val ok = bottomSheet.findViewById<Button>(R.id.okButton)
|
||||
ok?.setOnClickListener {
|
||||
downloadItem.extraCommands = text.text.toString()
|
||||
bottomSheet.dismiss()
|
||||
}
|
||||
|
||||
bottomSheet.show()
|
||||
val displayMetrics = DisplayMetrics()
|
||||
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
|
||||
bottomSheet.window!!.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
}else{
|
||||
extraCommands.visibility = View.GONE
|
||||
}
|
||||
}catch (e : Exception){
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,31 @@
|
|||
package com.deniscerri.ytdlnis.ui.downloadcard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.icu.text.IDNA.Info
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
|
|
@ -33,7 +40,9 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
|
|||
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
|
||||
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
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.dialog.MaterialAlertDialogBuilder
|
||||
|
|
@ -56,6 +65,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
|
|||
private lateinit var author : TextInputLayout
|
||||
private lateinit var saveDir : TextInputLayout
|
||||
private lateinit var freeSpace : TextView
|
||||
private lateinit var infoUtil: InfoUtil
|
||||
|
||||
lateinit var downloadItem: DownloadItem
|
||||
|
||||
|
|
@ -69,10 +79,12 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
|
|||
activity = getActivity()
|
||||
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
resultViewModel = ViewModelProvider(this@DownloadVideoFragment)[ResultViewModel::class.java]
|
||||
infoUtil = InfoUtil(requireContext())
|
||||
return fragmentView
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
|
|
@ -388,6 +400,45 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
|
|||
downloadItem.videoPreferences.removeAudio = removeAudio.isChecked
|
||||
}
|
||||
|
||||
val extraCommands = view.findViewById<Chip>(R.id.extra_commands)
|
||||
if (sharedPreferences.getBoolean("use_extra_commands", false)){
|
||||
extraCommands.visibility = View.VISIBLE
|
||||
extraCommands.setOnClickListener {
|
||||
val bottomSheet = BottomSheetDialog(requireContext())
|
||||
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
bottomSheet.setContentView(R.layout.extra_commands_bottom_sheet)
|
||||
|
||||
val text = bottomSheet.findViewById<EditText>(R.id.command)
|
||||
val currentCommand = infoUtil.buildYoutubeDLRequest(downloadItem).buildCommand().joinToString(" ")
|
||||
bottomSheet.findViewById<TextView>(R.id.currentText)?.text = currentCommand
|
||||
|
||||
text?.setText(downloadItem.extraCommands)
|
||||
|
||||
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
text!!.postDelayed({
|
||||
text.setSelection(text.length())
|
||||
text.requestFocus()
|
||||
imm.showSoftInput(text, 0)
|
||||
}, 300)
|
||||
|
||||
val ok = bottomSheet.findViewById<Button>(R.id.okButton)
|
||||
ok?.setOnClickListener {
|
||||
downloadItem.extraCommands = text.text.toString()
|
||||
bottomSheet.dismiss()
|
||||
}
|
||||
|
||||
bottomSheet.show()
|
||||
val displayMetrics = DisplayMetrics()
|
||||
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
|
||||
bottomSheet.window!!.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
}else{
|
||||
extraCommands.visibility = View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@ import com.deniscerri.ytdlnis.R
|
|||
import com.deniscerri.ytdlnis.adapter.CookieAdapter
|
||||
import com.deniscerri.ytdlnis.database.models.CookieItem
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.deniscerri.ytdlnis.database.models.LogItem
|
|||
import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.neo.highlight.core.Highlight
|
||||
import com.neo.highlight.util.listener.HighlightTextWatcher
|
||||
import com.neo.highlight.util.scheme.ColorScheme
|
||||
|
|
@ -65,6 +66,7 @@ class DownloadLogFragment : Fragment() {
|
|||
val clipboard: ClipboardManager =
|
||||
mainActivity.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setText(content.text)
|
||||
Snackbar.make(content, getString(R.string.copied_to_clipboard), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
val id = arguments?.getLong("logID")
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
package com.deniscerri.ytdlnis.ui.more.downloadLogs
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.deniscerri.ytdlnis.MainActivity
|
||||
|
|
@ -20,17 +28,22 @@ import com.deniscerri.ytdlnis.database.models.LogItem
|
|||
import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import java.io.File
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListener {
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var downloadLogAdapter: DownloadLogsAdapter
|
||||
private lateinit var noResults: RelativeLayout
|
||||
private lateinit var fileList: MutableList<File>
|
||||
private lateinit var items: MutableList<LogItem>
|
||||
private lateinit var topAppBar: MaterialToolbar
|
||||
private lateinit var mainActivity: MainActivity
|
||||
private lateinit var logViewModel: LogViewModel
|
||||
private var selectedObjects: ArrayList<LogItem>? = null
|
||||
private var actionMode : ActionMode? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
|
@ -39,6 +52,8 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
|
|||
): View? {
|
||||
mainActivity = activity as MainActivity
|
||||
mainActivity.hideBottomNavigation()
|
||||
items = mutableListOf()
|
||||
selectedObjects = arrayListOf()
|
||||
return inflater.inflate(R.layout.fragment_download_log_list, container, false)
|
||||
}
|
||||
|
||||
|
|
@ -55,11 +70,18 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
|
|||
recyclerView = view.findViewById(R.id.logs_recyclerview)
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
recyclerView.adapter = downloadLogAdapter
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
if (preferences.getBoolean("swipe_gestures", true)){
|
||||
val itemTouchHelper = ItemTouchHelper(simpleCallback)
|
||||
itemTouchHelper.attachToRecyclerView(recyclerView)
|
||||
}
|
||||
|
||||
noResults = view.findViewById(R.id.no_results)
|
||||
noResults.visibility = View.GONE
|
||||
|
||||
logViewModel = ViewModelProvider(this)[LogViewModel::class.java]
|
||||
logViewModel.items.observe(viewLifecycleOwner) {
|
||||
items = it.toMutableList()
|
||||
if (it.isEmpty()) noResults.visibility = View.VISIBLE
|
||||
else noResults.visibility = View.GONE
|
||||
downloadLogAdapter.submitList(it)
|
||||
|
|
@ -89,9 +111,9 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
|
|||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: LogItem) {
|
||||
override fun onItemClick(itemID: Long) {
|
||||
val bundle = Bundle()
|
||||
bundle.putLong("logID", item.id)
|
||||
bundle.putLong("logID", itemID)
|
||||
findNavController().navigate(
|
||||
R.id.downloadLogFragment,
|
||||
bundle
|
||||
|
|
@ -108,4 +130,151 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
|
|||
}
|
||||
deleteDialog.show()
|
||||
}
|
||||
|
||||
override fun onCardSelect(itemID: Long, isChecked: Boolean) {
|
||||
val item = items.find { it.id == itemID }
|
||||
if (isChecked) {
|
||||
selectedObjects!!.add(item!!)
|
||||
if (actionMode == null){
|
||||
actionMode = (activity 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 val contextualActionBar = object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
mode!!.menuInflater.inflate(R.menu.logs_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!!){
|
||||
logViewModel.delete(obj)
|
||||
}
|
||||
clearCheckedItems()
|
||||
actionMode?.finish()
|
||||
}
|
||||
deleteDialog.show()
|
||||
true
|
||||
}
|
||||
R.id.select_all -> {
|
||||
downloadLogAdapter.checkAll(items)
|
||||
selectedObjects?.clear()
|
||||
items.forEach { selectedObjects?.add(it) }
|
||||
mode?.title = getString(R.string.all_items_selected)
|
||||
true
|
||||
}
|
||||
R.id.invert_selected -> {
|
||||
downloadLogAdapter.invertSelected(items)
|
||||
val invertedList = arrayListOf<LogItem>()
|
||||
items.forEach {
|
||||
if (!selectedObjects?.contains(it)!!) invertedList.add(it)
|
||||
}
|
||||
selectedObjects?.clear()
|
||||
selectedObjects?.addAll(invertedList)
|
||||
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
|
||||
if (invertedList.isEmpty()) actionMode?.finish()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
actionMode = null
|
||||
clearCheckedItems()
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCheckedItems(){
|
||||
downloadLogAdapter.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
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val position = viewHolder.bindingAdapterPosition
|
||||
when (direction) {
|
||||
ItemTouchHelper.LEFT -> {
|
||||
val deletedItem = items[position]
|
||||
logViewModel.delete(deletedItem)
|
||||
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
|
||||
.setAction(getString(R.string.undo)) {
|
||||
lifecycleScope.launch(Dispatchers.IO){
|
||||
logViewModel.insert(deletedItem)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChildDraw(
|
||||
c: Canvas,
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
dX: Float,
|
||||
dY: Float,
|
||||
actionState: Int,
|
||||
isCurrentlyActive: Boolean
|
||||
) {
|
||||
RecyclerViewSwipeDecorator.Builder(
|
||||
requireContext(),
|
||||
c,
|
||||
recyclerView,
|
||||
viewHolder,
|
||||
dX,
|
||||
dY,
|
||||
actionState,
|
||||
isCurrentlyActive
|
||||
)
|
||||
.addSwipeLeftBackgroundColor(Color.RED)
|
||||
.addSwipeLeftActionIcon(R.drawable.baseline_delete_24)
|
||||
.create()
|
||||
.decorate()
|
||||
super.onChildDraw(
|
||||
c,
|
||||
recyclerView,
|
||||
viewHolder,
|
||||
dX,
|
||||
dY,
|
||||
actionState,
|
||||
isCurrentlyActive
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -200,7 +200,10 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
version = findPreference("version")
|
||||
version!!.summary = "${BuildConfig.VERSION_NAME} [${Build.SUPPORTED_ABIS[0]}] ${BuildConfig.BUILD_TYPE}"
|
||||
val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir
|
||||
val installedABI = Build.SUPPORTED_ABIS.first { nativeLibraryDir!!.contains(it) }
|
||||
|
||||
version!!.summary = "${BuildConfig.VERSION_NAME} [${installedABI}] ${BuildConfig.BUILD_TYPE}"
|
||||
|
||||
version!!.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ import android.widget.Toast
|
|||
import androidx.preference.PreferenceManager
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.models.ChapterItem
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.models.Format
|
||||
import com.deniscerri.ytdlnis.database.models.ResultItem
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.work.DownloadWorker
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import com.yausername.youtubedl_android.YoutubeDLRequest
|
||||
|
|
@ -560,9 +562,16 @@ class InfoUtil(private val context: Context) {
|
|||
val formats : ArrayList<Format> = ArrayList()
|
||||
for (f in 0 until jsonArray.length()){
|
||||
val format = jsonArray.getJSONObject(f)
|
||||
if (format.has("filesize") && format.get("filesize") == "None"){
|
||||
format.remove("filesize")
|
||||
format.put("filesize", 0)
|
||||
if (format.has("filesize")){
|
||||
if (format.get("filesize") == "None"){
|
||||
format.remove("filesize")
|
||||
format.put("filesize", 0)
|
||||
}
|
||||
try{
|
||||
val size = format.get("filesize").toString().toFloat()
|
||||
format.remove("filesize")
|
||||
format.put("filesize", size)
|
||||
}catch (ignored: Exception){}
|
||||
}
|
||||
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
|
||||
if (format.has("format_note")){
|
||||
|
|
@ -626,9 +635,16 @@ class InfoUtil(private val context: Context) {
|
|||
try{
|
||||
if (format.getString("filesize") == "None") continue
|
||||
}catch (e: Exception) { continue }
|
||||
if (format.has("filesize") && format.get("filesize") == "None"){
|
||||
format.remove("filesize")
|
||||
format.put("filesize", 0)
|
||||
if (format.has("filesize")){
|
||||
if (format.get("filesize") == "None"){
|
||||
format.remove("filesize")
|
||||
format.put("filesize", 0)
|
||||
}
|
||||
try{
|
||||
val size = format.get("filesize").toString().toFloat()
|
||||
format.remove("filesize")
|
||||
format.put("filesize", size)
|
||||
}catch (ignored: Exception){}
|
||||
}
|
||||
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
|
||||
if ( !formatProper!!.format_note.contains("audio only", true)) {
|
||||
|
|
@ -733,9 +749,16 @@ class InfoUtil(private val context: Context) {
|
|||
if (formatsInJSON != null) {
|
||||
for (f in 0 until formatsInJSON.length()){
|
||||
val format = formatsInJSON.getJSONObject(f)
|
||||
if (format.has("filesize") && format.get("filesize") == "None"){
|
||||
format.remove("filesize")
|
||||
format.put("filesize", 0)
|
||||
if (format.has("filesize")){
|
||||
if (format.get("filesize") == "None"){
|
||||
format.remove("filesize")
|
||||
format.put("filesize", 0)
|
||||
}
|
||||
try{
|
||||
val size = format.get("filesize").toString().toFloat()
|
||||
format.remove("filesize")
|
||||
format.put("filesize", size)
|
||||
}catch (ignored: Exception){}
|
||||
}
|
||||
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
|
||||
if (formatProper.format_note == null) continue
|
||||
|
|
@ -768,7 +791,7 @@ class InfoUtil(private val context: Context) {
|
|||
items.add(ResultItem(0,
|
||||
url,
|
||||
title,
|
||||
author!!,
|
||||
author,
|
||||
duration,
|
||||
thumb!!,
|
||||
website,
|
||||
|
|
@ -844,7 +867,7 @@ class InfoUtil(private val context: Context) {
|
|||
}else{
|
||||
jsonObject.getString("title")
|
||||
},
|
||||
author!!,
|
||||
author,
|
||||
duration,
|
||||
thumb!!,
|
||||
jsonObject.getString("extractor"),
|
||||
|
|
@ -969,6 +992,244 @@ class InfoUtil(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{
|
||||
val tempFileDir = File(context.cacheDir.absolutePath + "/downloads/" + downloadItem.id)
|
||||
|
||||
val url = downloadItem.url
|
||||
val request = YoutubeDLRequest(url)
|
||||
val type = downloadItem.type
|
||||
|
||||
val aria2 = sharedPreferences.getBoolean("aria2", false)
|
||||
if (aria2) {
|
||||
request.addOption("--downloader", "libaria2c.so")
|
||||
request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"")
|
||||
} else {
|
||||
val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1)
|
||||
if (concurrentFragments > 1) request.addOption("-N", concurrentFragments)
|
||||
}
|
||||
|
||||
val retries = sharedPreferences.getString("--retries", "")!!
|
||||
val fragmentRetries = sharedPreferences.getString("--fragment_retries", "")!!
|
||||
|
||||
if(retries.isNotEmpty()) request.addOption("retries", retries)
|
||||
if(fragmentRetries.isNotEmpty()) request.addOption("fragment-retries", fragmentRetries)
|
||||
|
||||
val limitRate = sharedPreferences.getString("limit_rate", "")
|
||||
if (limitRate != "") request.addOption("-r", limitRate!!)
|
||||
if(downloadItem.type != DownloadViewModel.Type.command){
|
||||
if (downloadItem.SaveThumb) {
|
||||
request.addOption("--write-thumbnail")
|
||||
request.addOption("--convert-thumbnails", "png")
|
||||
}
|
||||
if (!sharedPreferences.getBoolean("mtime", false)){
|
||||
request.addOption("--no-mtime")
|
||||
}
|
||||
|
||||
val sponsorBlockFilters : ArrayList<String> = when(downloadItem.type) {
|
||||
DownloadViewModel.Type.audio -> {
|
||||
downloadItem.audioPreferences.sponsorBlockFilters
|
||||
}
|
||||
//video
|
||||
else -> {
|
||||
downloadItem.videoPreferences.sponsorBlockFilters
|
||||
}
|
||||
}
|
||||
|
||||
if (sponsorBlockFilters.isNotEmpty()) {
|
||||
val filters = java.lang.String.join(",", sponsorBlockFilters.filter { it.isNotBlank() })
|
||||
if (filters.isNotBlank()) request.addOption("--sponsorblock-remove", filters)
|
||||
}
|
||||
|
||||
if(downloadItem.title.isNotBlank()){
|
||||
request.addCommands(listOf("--replace-in-metadata","title",".*.",downloadItem.title.take(200)))
|
||||
}
|
||||
if (downloadItem.author.isNotBlank()){
|
||||
request.addCommands(listOf("--replace-in-metadata","uploader",".*.",downloadItem.author.take(25)))
|
||||
}
|
||||
request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$",""))
|
||||
if (downloadItem.customFileNameTemplate.isBlank()) downloadItem.customFileNameTemplate = "%(uploader)s - %(title)s"
|
||||
|
||||
if (downloadItem.downloadSections.isNotBlank()){
|
||||
downloadItem.downloadSections.split(";").forEach {
|
||||
if (it.isBlank()) return@forEach
|
||||
if (it.contains(":"))
|
||||
request.addOption("--download-sections", "*$it")
|
||||
else
|
||||
request.addOption("--download-sections", it)
|
||||
|
||||
if (sharedPreferences.getBoolean("force_keyframes", false)){
|
||||
request.addOption("--force-keyframes-at-cuts")
|
||||
}
|
||||
}
|
||||
downloadItem.customFileNameTemplate += " %(section_title)s %(autonumber)s"
|
||||
request.addOption("--output-na-placeholder", " ")
|
||||
}
|
||||
|
||||
if (sharedPreferences.getBoolean("use_audio_quality", false)){
|
||||
request.addOption("--audio-quality", sharedPreferences.getInt("audio_quality", 0))
|
||||
}
|
||||
|
||||
if (downloadItem.extraCommands.isNotBlank()){
|
||||
val conf = File(context.cacheDir.absolutePath + "/downloads/configExtraCommands${System.currentTimeMillis()}.txt")
|
||||
conf.createNewFile()
|
||||
conf.writeText(downloadItem.extraCommands)
|
||||
request.addOption(
|
||||
"--config-locations",
|
||||
conf.absolutePath
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (sharedPreferences.getBoolean("restrict_filenames", true)) {
|
||||
request.addOption("--restrict-filenames")
|
||||
}
|
||||
|
||||
val cookiesFile = File(context.cacheDir, "cookies.txt")
|
||||
if (cookiesFile.exists()){
|
||||
request.addOption("--cookies", cookiesFile.absolutePath)
|
||||
}
|
||||
|
||||
val proxy = sharedPreferences.getString("proxy", "")
|
||||
if (proxy!!.isNotBlank()){
|
||||
request.addOption("--proxy", proxy)
|
||||
}
|
||||
|
||||
val keepCache = sharedPreferences.getBoolean("keep_cache", false)
|
||||
if(keepCache){
|
||||
request.addOption("--part")
|
||||
request.addOption("--keep-fragments")
|
||||
}
|
||||
|
||||
when(type){
|
||||
DownloadViewModel.Type.audio -> {
|
||||
val supportedContainers = context.resources.getStringArray(R.array.audio_containers)
|
||||
|
||||
var audioQualityId : String = downloadItem.format.format_id
|
||||
if (audioQualityId.isBlank() || audioQualityId == "0" || audioQualityId == context.getString(R.string.best_quality) || audioQualityId == "best") audioQualityId = ""
|
||||
else if (audioQualityId == context.getString(R.string.worst_quality) || audioQualityId == "worst") audioQualityId = "worstaudio"
|
||||
|
||||
val ext = downloadItem.container
|
||||
if (audioQualityId.isNotBlank()) request.addOption("-f", audioQualityId)
|
||||
request.addOption("-x")
|
||||
|
||||
if(ext.isNotBlank()){
|
||||
if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){
|
||||
request.addOption("--audio-format", ext)
|
||||
}
|
||||
}
|
||||
|
||||
request.addOption("--embed-metadata")
|
||||
|
||||
if (downloadItem.audioPreferences.embedThumb) {
|
||||
request.addOption("--embed-thumbnail")
|
||||
request.addOption("--convert-thumbnails", "jpg")
|
||||
if (sharedPreferences.getBoolean("crop_thumbnail", true)){
|
||||
try {
|
||||
val config = File(context.cacheDir.absolutePath + "/downloads/${downloadItem.id}/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt")
|
||||
val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
|
||||
config.writeText(configData)
|
||||
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
|
||||
request.addOption("--config", config.absolutePath)
|
||||
} catch (ignored: Exception) {}
|
||||
}
|
||||
}
|
||||
request.addOption("--parse-metadata", "%(release_year,upload_date)s:%(meta_date)s")
|
||||
|
||||
if (downloadItem.playlistTitle.isNotEmpty()) {
|
||||
request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s")
|
||||
request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(meta_track)s")
|
||||
} else {
|
||||
request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
|
||||
}
|
||||
|
||||
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
|
||||
request.addOption("--split-chapters")
|
||||
request.addOption("-P", tempFileDir.absolutePath)
|
||||
}else{
|
||||
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
|
||||
}
|
||||
|
||||
}
|
||||
DownloadViewModel.Type.video -> {
|
||||
val supportedContainers = context.resources.getStringArray(R.array.video_containers)
|
||||
|
||||
if (downloadItem.videoPreferences.addChapters) {
|
||||
request.addOption("--sponsorblock-mark", "all")
|
||||
request.addOption("--embed-chapters")
|
||||
}
|
||||
if (downloadItem.videoPreferences.embedSubs) {
|
||||
request.addOption("--embed-subs")
|
||||
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
|
||||
}
|
||||
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(DownloadWorker.TAG, videoFormatID)
|
||||
var formatArgument = if (downloadItem.videoPreferences.removeAudio) "bestvideo" else "bestvideo+bestaudio/best"
|
||||
if (videoFormatID.isNotEmpty()) {
|
||||
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)+"]"
|
||||
|
||||
formatArgument = if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty() && ! downloadItem.videoPreferences.removeAudio){
|
||||
val audioIds = downloadItem.videoPreferences.audioFormatIDs.joinToString("+")
|
||||
"$videoFormatID+$audioIds/best/$videoFormatID"
|
||||
}else{
|
||||
"$videoFormatID+bestaudio/best/$videoFormatID"
|
||||
}
|
||||
}
|
||||
Log.e(DownloadWorker.TAG, formatArgument)
|
||||
request.addOption("-f", formatArgument)
|
||||
val outputFormat = downloadItem.container
|
||||
if(outputFormat.isNotEmpty() && outputFormat != "Default" && outputFormat != context.getString(R.string.defaultValue) && supportedContainers.contains(outputFormat)){
|
||||
request.addOption("--merge-output-format", outputFormat.lowercase())
|
||||
if (outputFormat != "webm") {
|
||||
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
|
||||
if (embedThumb) {
|
||||
request.addOption("--embed-thumbnail")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadItem.videoPreferences.writeSubs){
|
||||
request.addOption("--write-subs")
|
||||
request.addOption("--write-auto-subs")
|
||||
request.addOption("--sub-format", "str/ass/best")
|
||||
request.addOption("--convert-subtitles", "srt")
|
||||
if (!downloadItem.videoPreferences.embedSubs) {
|
||||
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadItem.videoPreferences.removeAudio){
|
||||
request.addOption("--ppa", "ffmpeg:-an")
|
||||
}
|
||||
|
||||
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
|
||||
request.addOption("--split-chapters")
|
||||
request.addOption("-P", tempFileDir.absolutePath)
|
||||
}else{
|
||||
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
|
||||
}
|
||||
|
||||
}
|
||||
DownloadViewModel.Type.command -> {
|
||||
request.addOption(
|
||||
"--config-locations",
|
||||
File(context.cacheDir.absolutePath + "/downloads/config${System.currentTimeMillis()}.txt").apply {
|
||||
writeText(downloadItem.format.format_note)
|
||||
}.absolutePath
|
||||
)
|
||||
request.addOption("-P", tempFileDir.absolutePath)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
private val pipedURL = sharedPreferences.getString("piped_instance", defaultPipedURL)?.ifEmpty { defaultPipedURL }?.removeSuffix("/")
|
||||
|
||||
class PlaylistTuple internal constructor(
|
||||
|
|
|
|||
|
|
@ -21,12 +21,10 @@ import com.deniscerri.ytdlnis.database.models.HistoryItem
|
|||
import com.deniscerri.ytdlnis.database.models.LogItem
|
||||
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
|
||||
import com.deniscerri.ytdlnis.database.repository.LogRepository
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.deniscerri.ytdlnis.util.NotificationUtil
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import com.yausername.youtubedl_android.YoutubeDLRequest
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -42,6 +40,7 @@ class DownloadWorker(
|
|||
itemId = inputData.getLong("id", 0)
|
||||
if (itemId == 0L) return Result.failure()
|
||||
|
||||
|
||||
val notificationUtil = NotificationUtil(context)
|
||||
val infoUtil = InfoUtil(context)
|
||||
val dbManager = DBManager.getInstance(context)
|
||||
|
|
@ -52,7 +51,6 @@ class DownloadWorker(
|
|||
val logRepo = LogRepository(dbManager.logDao)
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
|
||||
val downloadItem: DownloadItem?
|
||||
try {
|
||||
downloadItem = repository.getItemByID(itemId)
|
||||
|
|
@ -63,6 +61,15 @@ class DownloadWorker(
|
|||
|
||||
if (downloadItem.status != DownloadRepository.Status.Queued.toString()) return Result.failure()
|
||||
|
||||
val pendingIntent = NavDeepLinkBuilder(context)
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.downloadQueueMainFragment)
|
||||
.createPendingIntent()
|
||||
|
||||
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt(), NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
|
||||
val foregroundInfo = ForegroundInfo(downloadItem.id.toInt(), notification)
|
||||
setForegroundAsync(foregroundInfo)
|
||||
|
||||
Log.e(TAG, downloadItem.toString())
|
||||
|
||||
runBlocking{
|
||||
|
|
@ -75,254 +82,28 @@ class DownloadWorker(
|
|||
updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
|
||||
}
|
||||
|
||||
val pendingIntent = NavDeepLinkBuilder(context)
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.downloadQueueMainFragment)
|
||||
.createPendingIntent()
|
||||
|
||||
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt(), NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
|
||||
val foregroundInfo = ForegroundInfo(downloadItem.id.toInt(), notification)
|
||||
setForegroundAsync(foregroundInfo)
|
||||
|
||||
|
||||
val url = downloadItem.url
|
||||
val request = YoutubeDLRequest(url)
|
||||
val type = downloadItem.type
|
||||
val downloadLocation = downloadItem.downloadPath
|
||||
|
||||
val request = infoUtil.buildYoutubeDLRequest(downloadItem)
|
||||
val tempFileDir = File(context.cacheDir.absolutePath + "/downloads/" + downloadItem.id)
|
||||
tempFileDir.delete()
|
||||
tempFileDir.mkdirs()
|
||||
|
||||
val aria2 = sharedPreferences.getBoolean("aria2", false)
|
||||
if (aria2) {
|
||||
request.addOption("--downloader", "libaria2c.so")
|
||||
request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"")
|
||||
} else {
|
||||
val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1)
|
||||
if (concurrentFragments > 1) request.addOption("-N", concurrentFragments)
|
||||
}
|
||||
|
||||
val retries = sharedPreferences.getString("--retries", "")!!
|
||||
val fragmentRetries = sharedPreferences.getString("--fragment_retries", "")!!
|
||||
|
||||
if(retries.isNotEmpty()) request.addOption("retries", retries)
|
||||
if(fragmentRetries.isNotEmpty()) request.addOption("fragment-retries", fragmentRetries)
|
||||
|
||||
val limitRate = sharedPreferences.getString("limit_rate", "")
|
||||
if (limitRate != "") request.addOption("-r", limitRate!!)
|
||||
if(downloadItem.type != DownloadViewModel.Type.command){
|
||||
if (downloadItem.SaveThumb) {
|
||||
request.addOption("--write-thumbnail")
|
||||
request.addOption("--convert-thumbnails", "png")
|
||||
}
|
||||
if (!sharedPreferences.getBoolean("mtime", false)){
|
||||
request.addOption("--no-mtime")
|
||||
}
|
||||
|
||||
val sponsorBlockFilters : ArrayList<String> = when(downloadItem.type) {
|
||||
DownloadViewModel.Type.audio -> {
|
||||
downloadItem.audioPreferences.sponsorBlockFilters
|
||||
}
|
||||
//video
|
||||
else -> {
|
||||
downloadItem.videoPreferences.sponsorBlockFilters
|
||||
}
|
||||
}
|
||||
|
||||
if (sponsorBlockFilters.isNotEmpty()) {
|
||||
val filters = java.lang.String.join(",", sponsorBlockFilters.filter { it.isNotBlank() })
|
||||
if (filters.isNotBlank()) request.addOption("--sponsorblock-remove", filters)
|
||||
}
|
||||
|
||||
if(downloadItem.title.isNotBlank()){
|
||||
request.addCommands(listOf("--replace-in-metadata","title",".*.",downloadItem.title.take(200)))
|
||||
}
|
||||
if (downloadItem.author.isNotBlank()){
|
||||
request.addCommands(listOf("--replace-in-metadata","uploader",".*.",downloadItem.author.take(25)))
|
||||
}
|
||||
request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$",""))
|
||||
if (downloadItem.customFileNameTemplate.isBlank()) downloadItem.customFileNameTemplate = "%(uploader)s - %(title)s"
|
||||
|
||||
if (downloadItem.downloadSections.isNotBlank()){
|
||||
downloadItem.downloadSections.split(";").forEach {
|
||||
if (it.isBlank()) return@forEach
|
||||
if (it.contains(":"))
|
||||
request.addOption("--download-sections", "*$it")
|
||||
else
|
||||
request.addOption("--download-sections", it)
|
||||
|
||||
if (sharedPreferences.getBoolean("force_keyframes", false)){
|
||||
request.addOption("--force-keyframes-at-cuts")
|
||||
}
|
||||
}
|
||||
downloadItem.customFileNameTemplate += " %(section_title)s %(autonumber)s"
|
||||
request.addOption("--output-na-placeholder", " ")
|
||||
}
|
||||
|
||||
if (sharedPreferences.getBoolean("use_audio_quality", false)){
|
||||
request.addOption("--audio-quality", sharedPreferences.getInt("audio_quality", 0))
|
||||
}
|
||||
}
|
||||
|
||||
if (sharedPreferences.getBoolean("restrict_filenames", true)) {
|
||||
request.addOption("--restrict-filenames")
|
||||
}
|
||||
|
||||
val cookiesFile = File(context.cacheDir, "cookies.txt")
|
||||
if (cookiesFile.exists()){
|
||||
request.addOption("--cookies", cookiesFile.absolutePath)
|
||||
}
|
||||
|
||||
val proxy = sharedPreferences.getString("proxy", "")
|
||||
if (proxy!!.isNotBlank()){
|
||||
request.addOption("--proxy", proxy)
|
||||
}
|
||||
|
||||
val downloadLocation = downloadItem.downloadPath
|
||||
val keepCache = sharedPreferences.getBoolean("keep_cache", false)
|
||||
if(keepCache){
|
||||
request.addOption("--part")
|
||||
request.addOption("--keep-fragments")
|
||||
}
|
||||
|
||||
when(type){
|
||||
DownloadViewModel.Type.audio -> {
|
||||
val supportedContainers = context.resources.getStringArray(R.array.audio_containers)
|
||||
|
||||
var audioQualityId : String = downloadItem.format.format_id
|
||||
if (audioQualityId.isBlank() || audioQualityId == "0" || audioQualityId == context.getString(R.string.best_quality) || audioQualityId == "best") audioQualityId = ""
|
||||
else if (audioQualityId == context.getString(R.string.worst_quality) || audioQualityId == "worst") audioQualityId = "worstaudio"
|
||||
|
||||
val ext = downloadItem.container
|
||||
if (audioQualityId.isNotBlank()) request.addOption("-f", audioQualityId)
|
||||
request.addOption("-x")
|
||||
|
||||
if(ext.isNotBlank()){
|
||||
if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){
|
||||
request.addOption("--audio-format", ext)
|
||||
}
|
||||
}
|
||||
|
||||
request.addOption("--embed-metadata")
|
||||
|
||||
if (downloadItem.audioPreferences.embedThumb) {
|
||||
request.addOption("--embed-thumbnail")
|
||||
request.addOption("--convert-thumbnails", "jpg")
|
||||
if (sharedPreferences.getBoolean("crop_thumbnail", true)){
|
||||
try {
|
||||
val config = File(context.cacheDir.absolutePath + "/downloads/${downloadItem.id}/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt")
|
||||
val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
|
||||
config.writeText(configData)
|
||||
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
|
||||
request.addOption("--config", config.absolutePath)
|
||||
} catch (ignored: Exception) {}
|
||||
}
|
||||
}
|
||||
request.addOption("--parse-metadata", "%(release_year,upload_date)s:%(meta_date)s")
|
||||
|
||||
if (downloadItem.playlistTitle.isNotEmpty()) {
|
||||
request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s")
|
||||
request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(meta_track)s")
|
||||
} else {
|
||||
request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
|
||||
}
|
||||
|
||||
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
|
||||
request.addOption("--split-chapters")
|
||||
request.addOption("-P", tempFileDir.absolutePath)
|
||||
}else{
|
||||
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
|
||||
}
|
||||
|
||||
}
|
||||
DownloadViewModel.Type.video -> {
|
||||
val supportedContainers = context.resources.getStringArray(R.array.video_containers)
|
||||
|
||||
if (downloadItem.videoPreferences.addChapters) {
|
||||
request.addOption("--sponsorblock-mark", "all")
|
||||
request.addOption("--embed-chapters")
|
||||
}
|
||||
if (downloadItem.videoPreferences.embedSubs) {
|
||||
request.addOption("--embed-subs")
|
||||
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
|
||||
}
|
||||
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"
|
||||
if (videoFormatID.isNotEmpty()) {
|
||||
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)+"]"
|
||||
|
||||
formatArgument = if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty() && ! downloadItem.videoPreferences.removeAudio){
|
||||
val audioIds = downloadItem.videoPreferences.audioFormatIDs.joinToString("+")
|
||||
"$videoFormatID+$audioIds/best/$videoFormatID"
|
||||
}else{
|
||||
"$videoFormatID+bestaudio/best/$videoFormatID"
|
||||
}
|
||||
}
|
||||
Log.e(TAG, formatArgument)
|
||||
request.addOption("-f", formatArgument)
|
||||
val outputFormat = downloadItem.container
|
||||
if(outputFormat.isNotEmpty() && outputFormat != "Default" && outputFormat != context.getString(R.string.defaultValue) && supportedContainers.contains(outputFormat)){
|
||||
request.addOption("--merge-output-format", outputFormat.lowercase())
|
||||
if (outputFormat != "webm") {
|
||||
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
|
||||
if (embedThumb) {
|
||||
request.addOption("--embed-thumbnail")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadItem.videoPreferences.writeSubs){
|
||||
request.addOption("--write-subs")
|
||||
request.addOption("--write-auto-subs")
|
||||
request.addOption("--sub-format", "str/ass/best")
|
||||
request.addOption("--convert-subtitles", "srt")
|
||||
if (!downloadItem.videoPreferences.embedSubs) {
|
||||
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadItem.videoPreferences.removeAudio){
|
||||
request.addOption("--ppa", "ffmpeg:-an")
|
||||
}
|
||||
|
||||
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
|
||||
request.addOption("--split-chapters")
|
||||
request.addOption("-P", tempFileDir.absolutePath)
|
||||
}else{
|
||||
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
|
||||
}
|
||||
|
||||
}
|
||||
DownloadViewModel.Type.command -> {
|
||||
request.addOption(
|
||||
"--config-locations",
|
||||
File(context.cacheDir.absolutePath + "/downloads/config${System.currentTimeMillis()}.txt").apply {
|
||||
writeText(downloadItem.format.format_note)
|
||||
}.absolutePath
|
||||
)
|
||||
request.addOption("-P", tempFileDir.absolutePath)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
|
||||
|
||||
val logItem = LogItem(
|
||||
0,
|
||||
downloadItem.title,
|
||||
downloadItem.title.ifEmpty { downloadItem.url },
|
||||
"Downloading:\n" +
|
||||
"Title: ${downloadItem.title}\n" +
|
||||
"URL: ${downloadItem.url}\n" +
|
||||
"Type: ${downloadItem.type}\n" +
|
||||
"Format: ${downloadItem.format}\n\n" +
|
||||
"Command: ${java.lang.String.join(" ", request.buildCommand())}\n\n"
|
||||
"Command: ${java.lang.String.join(" ", request.buildCommand())}\n\n",
|
||||
downloadItem.format,
|
||||
downloadItem.type,
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ import androidx.work.workDataOf
|
|||
import com.deniscerri.ytdlnis.MainActivity
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.DBManager
|
||||
import com.deniscerri.ytdlnis.database.models.Format
|
||||
import com.deniscerri.ytdlnis.database.models.LogItem
|
||||
import com.deniscerri.ytdlnis.database.repository.LogRepository
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.ui.more.TerminalActivity
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.NotificationUtil
|
||||
|
|
@ -83,10 +85,13 @@ class TerminalDownloadWorker(
|
|||
|
||||
val logItem = LogItem(
|
||||
0,
|
||||
"Terminal Download at " + Calendar.getInstance().time,
|
||||
"Terminal Download",
|
||||
"Downloading:\n" +
|
||||
"Terminal Download\n" +
|
||||
"Command: ${command}\n\n"
|
||||
"Command: ${command}\n\n",
|
||||
Format(),
|
||||
DownloadViewModel.Type.command,
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
runCatching {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" >
|
||||
|
||||
<com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
|
||||
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/video_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
@ -159,7 +161,7 @@
|
|||
app:layout_constraintStart_toEndOf="@+id/colon">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/download_log_item_constraint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="15dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:clickable="true"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:padding="10dp"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/delete"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:focusable="true" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/delete"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||
android:layout_width="wrap_content"
|
||||
android:padding="15dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_delete_all"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
90
app/src/main/res/layout/extra_commands_bottom_sheet.xml
Normal file
90
app/src/main/res/layout/extra_commands_bottom_sheet.xml
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="20dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/use_extra_commands"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/current"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginVertical="10dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:padding="10dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/currentTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/current"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/currentText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/current">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/command"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:fontFamily="monospace"
|
||||
android:maxLines="2000" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/okButton"
|
||||
android:layout_marginVertical="10dp"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ok"
|
||||
app:icon="@drawable/ic_check"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</ScrollView>
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/format"
|
||||
android:textSize="12sp"
|
||||
android:textSize="14sp"
|
||||
android:layout_margin="20dp"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
|
|
|
|||
|
|
@ -218,6 +218,22 @@
|
|||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/extra_commands"
|
||||
android:visibility="gone"
|
||||
style="@style/Widget.Material3.Chip.Assist.Elevated"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/use_extra_commands"
|
||||
app:chipIconVisible="true"
|
||||
app:chipIcon="@drawable/ic_terminal"
|
||||
android:minWidth="30dp"
|
||||
app:cornerRadius="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -296,6 +296,22 @@
|
|||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/extra_commands"
|
||||
android:visibility="gone"
|
||||
style="@style/Widget.Material3.Chip.Assist.Elevated"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/use_extra_commands"
|
||||
app:chipIconVisible="true"
|
||||
app:chipIcon="@drawable/ic_terminal"
|
||||
android:minWidth="30dp"
|
||||
app:cornerRadius="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
|
|
|||
201
app/src/main/res/layout/log_card.xml
Normal file
201
app/src/main/res/layout/log_card.xml
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView android:id="@+id/log_card_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@android:color/transparent"
|
||||
android:checkable="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:checkedIcon="@null"
|
||||
app:strokeColor="?attr/colorPrimary"
|
||||
app:cardPreventCornerOverlap="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
|
||||
app:strokeWidth="0dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingVertical="10dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/log_icon"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_baseline_file_open_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/download_item_data"
|
||||
app:layout_constraintHorizontal_weight="0.3"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/download_item_data"
|
||||
android:layout_width="0dp"
|
||||
android:clickable="false"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_weight="0.7"
|
||||
app:layout_constraintStart_toEndOf="@+id/log_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="false"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:scrollbars="none"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/action_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="title,action_button" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="bottom"
|
||||
android:clickable="false"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:scrollbars="none"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/barrier">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/downloadedTime"
|
||||
style="@style/Widget.Material3.FloatingActionButton.Large.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@drawable/rounded_corner"
|
||||
android:backgroundTint="?attr/colorPrimary"
|
||||
android:clickable="false"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:minWidth="30dp"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:textSize="12sp"
|
||||
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/format_note"
|
||||
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:maxLines="1"
|
||||
android:minWidth="30dp"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:textSize="12sp"
|
||||
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/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:gravity="center"
|
||||
android:minWidth="30dp"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:textSize="12sp"
|
||||
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.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:paddingHorizontal="5dp"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
app:cornerRadius="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/download_type"
|
||||
android:clickable="false"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="0dp"
|
||||
android:padding="0dp"
|
||||
app:cornerRadius="10dp"
|
||||
app:icon="@drawable/ic_video"
|
||||
app:iconTint="?attr/colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
23
app/src/main/res/menu/logs_menu_context.xml
Normal file
23
app/src/main/res/menu/logs_menu_context.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<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>
|
||||
|
||||
|
|
@ -273,7 +273,7 @@
|
|||
<string name="custom_audio_quality">Përdor cilësinë e personalizuar të audios</string>
|
||||
<string name="piped_instance">Shembull me tubacione</string>
|
||||
<string name="piped_instance_summary">Shkruani një Server API PIPED, aplikacioni mund të përdorë për kërkimet dhe formatet në youtube</string>
|
||||
<string name="retries">Riprovon</string>
|
||||
<string name="retries">Riprovimet</string>
|
||||
<string name="fragment_retries">Riprovimet e fragmentit</string>
|
||||
<string name="format_order">Renditja e formatit</string>
|
||||
</resources>
|
||||
|
|
@ -284,4 +284,7 @@
|
|||
<string name="format_order">Format Order</string>
|
||||
<string name="force_keyframes">Force Keyframes At Cuts</string>
|
||||
<string name="force_keyframes_summary">Accurate Video Cuts. Slower</string>
|
||||
<string name="use_extra_commands">Add Extra Commands</string>
|
||||
<string name="use_extra_commands_summary">For Audio / Video downloads. Add extra commands along with the GUI configuration</string>
|
||||
<string name="current">Current Command</string>
|
||||
</resources>
|
||||
|
|
@ -140,4 +140,13 @@
|
|||
app:defaultValue=""
|
||||
app:summary="@string/preferred_format_id_summary"
|
||||
app:title="@string/preferred_format_id" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:widgetLayout="@layout/preferece_material_switch"
|
||||
app:defaultValue="false"
|
||||
app:icon="@drawable/ic_terminal"
|
||||
app:key="use_extra_commands"
|
||||
android:summary="@string/use_extra_commands_summary"
|
||||
app:title="@string/use_extra_commands" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
<Preference
|
||||
android:key="downloading"
|
||||
app:icon="@drawable/ic_downloads"
|
||||
android:title="@string/downloading"/>
|
||||
android:title="@string/downloads"/>
|
||||
|
||||
<Preference
|
||||
android:key="processing"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ buildscript {
|
|||
retrofitVer = "2.9.0"
|
||||
kodeinVer = "7.16.0"
|
||||
navVer = "2.5.3"
|
||||
media3_version = "1.0.0"
|
||||
media3_version = "1.1.0"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
|
|
|||
Loading…
Reference in a new issue