implemented download queue

This commit is contained in:
Denis Çerri 2023-02-26 19:24:51 +01:00
parent 8eb0370c69
commit 8ff441172e
No known key found for this signature in database
GPG key ID: 95C43D517D830350
15 changed files with 796 additions and 222 deletions

View file

@ -120,7 +120,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:$appCompatVer" implementation "androidx.appcompat:appcompat:$appCompatVer"
implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation 'com.google.android.material:material:1.9.0-alpha02' implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.core:core:1.9.0' implementation 'androidx.core:core:1.9.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.2.1'

View file

@ -65,6 +65,16 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.DownloadQueueActivity"
android:configChanges="layoutDirection|locale"
android:parentActivityName=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="ytdlnis.DownloadQueueActivity" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity <activity
android:name=".ui.downloadLogs.DownloadLogActivity" android:name=".ui.downloadLogs.DownloadLogActivity"
android:configChanges="layoutDirection|locale" android:configChanges="layoutDirection|locale"

View file

@ -26,6 +26,7 @@ import androidx.work.WorkManager
import com.deniscerri.ytdlnis.databinding.ActivityMainBinding import com.deniscerri.ytdlnis.databinding.ActivityMainBinding
import com.deniscerri.ytdlnis.service.IDownloaderListener import com.deniscerri.ytdlnis.service.IDownloaderListener
import com.deniscerri.ytdlnis.service.IDownloaderService import com.deniscerri.ytdlnis.service.IDownloaderService
import com.deniscerri.ytdlnis.ui.DownloadQueueActivity
import com.deniscerri.ytdlnis.ui.HistoryFragment import com.deniscerri.ytdlnis.ui.HistoryFragment
import com.deniscerri.ytdlnis.ui.HomeFragment import com.deniscerri.ytdlnis.ui.HomeFragment
import com.deniscerri.ytdlnis.ui.MoreFragment import com.deniscerri.ytdlnis.ui.MoreFragment
@ -79,7 +80,8 @@ class MainActivity : AppCompatActivity() {
} }
R.id.history -> { R.id.history -> {
if (lastFragment === historyFragment) { if (lastFragment === historyFragment) {
historyFragment.scrollToTop() val intent = Intent(context, DownloadQueueActivity::class.java)
startActivity(intent)
} else { } else {
this.title = getString(R.string.downloads) this.title = getString(R.string.downloads)
} }

View file

@ -1,56 +1,48 @@
package com.deniscerri.ytdlnis.adapter package com.deniscerri.ytdlnis.adapter
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.graphics.Color import android.graphics.Color
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.models.DownloadItem
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.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<HistoryItem?, ActiveDownloadAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) { class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, ActiveDownloadAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) {
private val checkedItems: ArrayList<Long>
private val onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener
private val activity: Activity private val activity: Activity
private val fileUtil: FileUtil
init { init {
checkedItems = ArrayList()
this.onItemClickListener = onItemClickListener this.onItemClickListener = onItemClickListener
this.activity = activity this.activity = activity
fileUtil = FileUtil()
} }
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
val cardView: MaterialCardView val cardView: MaterialCardView
init { init {
cardView = itemView.findViewById(R.id.downloads_card_view) cardView = itemView.findViewById(R.id.active_download_card_view)
} }
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val cardView = LayoutInflater.from(parent.context) val cardView = LayoutInflater.from(parent.context)
.inflate(R.layout.history_card, parent, false) .inflate(R.layout.active_download_card, parent, false)
return ViewHolder(cardView, onItemClickListener) return ViewHolder(cardView, onItemClickListener)
} }
@ -58,7 +50,7 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
val item = getItem(position) val item = getItem(position)
val card = holder.cardView val card = holder.cardView
// THUMBNAIL ---------------------------------- // THUMBNAIL ----------------------------------
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view) val thumbnail = card.findViewById<ImageView>(R.id.image_view)
val imageURL = item!!.thumb val imageURL = item!!.thumb
val uiHandler = Handler(Looper.getMainLooper()) val uiHandler = Handler(Looper.getMainLooper())
if (imageURL.isNotEmpty()) { if (imageURL.isNotEmpty()) {
@ -68,126 +60,76 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
} }
thumbnail.setColorFilter(Color.argb(95, 0, 0, 0)) thumbnail.setColorFilter(Color.argb(95, 0, 0, 0))
// PROGRESS BAR ----------------------------------------------------
val progressBar = card.findViewById<LinearProgressIndicator>(R.id.progress)
progressBar.tag = "${item.id}##progress"
progressBar.progress = 0
progressBar.isIndeterminate = true
// TITLE ---------------------------------- // TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.downloads_title) val itemTitle = card.findViewById<TextView>(R.id.title)
var title = item.title var title = item.title
if (title.length > 100) { if (title.length > 100) {
title = title.substring(0, 40) + "..." title = title.substring(0, 40) + "..."
} }
itemTitle.text = title itemTitle.text = title
// Bottom Info ---------------------------------- // Author ----------------------------------
val bottomInfo = card.findViewById<TextView>(R.id.downloads_info_bottom) val author = card.findViewById<TextView>(R.id.author)
var info = item.author var info = item.author
if (item.duration.isNotEmpty()) { if (item.duration.isNotEmpty()) {
if (item.author.isNotEmpty()) info += "" if (item.author.isNotEmpty()) info += ""
info += item.duration info += item.duration
} }
bottomInfo.text = info author.text = info
// TIME DOWNLOADED ---------------------------------- val formatNote = card.findViewById<TextView>(R.id.format_note)
val datetime = card.findViewById<TextView>(R.id.downloads_info_time) formatNote.text = item.format.format_note
val time = item.time val codec = card.findViewById<TextView>(R.id.codec)
val downloadedTime: String val codecText =
if (time == 0L) { if (item.format.encoding != "") {
downloadedTime = activity.getString(R.string.currently_downloading) + " " + item.type item.format.encoding.uppercase()
} else { }else if (item.format.vcodec != "none" && item.format.vcodec != ""){
val cal = Calendar.getInstance() item.format.vcodec.uppercase()
val date = Date(time * 1000L)
cal.time = date
val day = cal[Calendar.DAY_OF_MONTH]
val month = cal.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault())
val year = cal[Calendar.YEAR]
val formatter: DateFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
val timeString = formatter.format(date)
downloadedTime = "$day $month $year - $timeString"
}
datetime.text = downloadedTime
// BUTTON ----------------------------------
val buttonLayout = card.findViewById<LinearLayout>(R.id.downloads_download_button_layout)
val btn = buttonLayout.findViewById<MaterialButton>(R.id.downloads_download_button_type)
var filePresent = true
//IS IN THE FILE SYSTEM?
val path = item.downloadPath
val file = File(path)
if (!file.exists() && path.isNotEmpty()) {
filePresent = false
thumbnail.colorFilter = ColorMatrixColorFilter(object : ColorMatrix() {
init {
setSaturation(0f)
}
})
thumbnail.alpha = 0.7f
}
if (item.type == DownloadViewModel.Type.audio) {
if (filePresent) btn.icon = ContextCompat.getDrawable(activity, R.drawable.ic_music_downloaded) else btn.icon = ContextCompat.getDrawable(activity, R.drawable.ic_music)
} else {
if (filePresent) btn.icon = ContextCompat.getDrawable(activity, R.drawable.ic_video_downloaded) else btn.icon = ContextCompat.getDrawable(activity, R.drawable.ic_video)
}
if (btn.hasOnClickListeners()) btn.setOnClickListener(null)
if (checkedItems.contains(item.id)) {
card.isChecked = true
card.strokeWidth = 5
} else {
card.isChecked = false
card.strokeWidth = 0
}
val finalFilePresent = filePresent
card.setOnLongClickListener {
checkCard(card, item.id)
true
}
card.setOnClickListener {
if (checkedItems.size > 0) {
checkCard(card, item.id)
} else { } else {
onItemClickListener.onCardClick(item.id, finalFilePresent) 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)
fileSize.text = fileUtil.convertFileSize(item.format.filesize)
//OUTPUT
val output = card.findViewById<TextView>(R.id.output)
output.tag = "${item.id}##output"
// CANCEL BUTTON ----------------------------------
val cancelButton = card.findViewById<MaterialButton>(R.id.active_download_cancel)
if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null)
cancelButton.setOnClickListener {
onItemClickListener.onCancelClick(item.id)
} }
} }
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 { interface OnItemClickListener {
fun onCardClick(itemID: Long, isPresent: Boolean) fun onCancelClick(itemID: Long)
fun onCardSelect(itemID: Long, isChecked: Boolean)
fun onButtonClick(position: Int)
}
@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()
} }
companion object { companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<HistoryItem> = object : DiffUtil.ItemCallback<HistoryItem>() { private val DIFF_CALLBACK: DiffUtil.ItemCallback<DownloadItem> = object : DiffUtil.ItemCallback<DownloadItem>() {
override fun areItemsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean { override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
val ranged = arrayListOf(oldItem.id, newItem.id) val ranged = arrayListOf(oldItem.id, newItem.id)
return ranged[0] == ranged[1] return ranged[0] == ranged[1]
} }
override fun areContentsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean { override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
return oldItem.time == newItem.time return oldItem.id == newItem.id
} }
} }
} }

View file

@ -7,6 +7,7 @@ import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter import android.graphics.ColorMatrixColorFilter
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -19,38 +20,40 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel 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.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import org.w3c.dom.Text
import java.io.File import java.io.File
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
class QueuedDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<HistoryItem?, QueuedDownloadAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) { class QueuedDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, QueuedDownloadAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) {
private val checkedItems: ArrayList<Long>
private val onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener
private val activity: Activity private val activity: Activity
private var fileUtil: FileUtil
init { init {
checkedItems = ArrayList()
this.onItemClickListener = onItemClickListener this.onItemClickListener = onItemClickListener
this.activity = activity this.activity = activity
fileUtil = FileUtil()
} }
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
val cardView: MaterialCardView val cardView: MaterialCardView
init { init {
cardView = itemView.findViewById(R.id.downloads_card_view) cardView = itemView.findViewById(R.id.queued_download_card_view)
} }
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val cardView = LayoutInflater.from(parent.context) val cardView = LayoutInflater.from(parent.context)
.inflate(R.layout.history_card, parent, false) .inflate(R.layout.queued_download_card, parent, false)
return ViewHolder(cardView, onItemClickListener) return ViewHolder(cardView, onItemClickListener)
} }
@ -58,7 +61,7 @@ class QueuedDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
val item = getItem(position) val item = getItem(position)
val card = holder.cardView val card = holder.cardView
// THUMBNAIL ---------------------------------- // THUMBNAIL ----------------------------------
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view) val thumbnail = card.findViewById<ImageView>(R.id.image_view)
val imageURL = item!!.thumb val imageURL = item!!.thumb
val uiHandler = Handler(Looper.getMainLooper()) val uiHandler = Handler(Looper.getMainLooper())
if (imageURL.isNotEmpty()) { if (imageURL.isNotEmpty()) {
@ -69,29 +72,48 @@ class QueuedDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
thumbnail.setColorFilter(Color.argb(95, 0, 0, 0)) thumbnail.setColorFilter(Color.argb(95, 0, 0, 0))
// TITLE ---------------------------------- // TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.downloads_title) val itemTitle = card.findViewById<TextView>(R.id.title)
var title = item.title var title = item.title
if (title.length > 100) { if (title.length > 100) {
title = title.substring(0, 40) + "..." title = title.substring(0, 40) + "..."
} }
itemTitle.text = title itemTitle.text = title
// Bottom Info ---------------------------------- // Author ----------------------------------
val bottomInfo = card.findViewById<TextView>(R.id.downloads_info_bottom) val author = card.findViewById<TextView>(R.id.author)
var info = item.author var info = item.author
if (item.duration.isNotEmpty()) { if (item.duration.isNotEmpty()) {
if (item.author.isNotEmpty()) info += "" if (item.author.isNotEmpty()) info += ""
info += item.duration info += item.duration
} }
bottomInfo.text = info author.text = info
// TIME DOWNLOADED ---------------------------------- val formatNote = card.findViewById<TextView>(R.id.format_note)
val datetime = card.findViewById<TextView>(R.id.downloads_info_time) formatNote.text = item.format.format_note
val time = item.time val codec = card.findViewById<TextView>(R.id.codec)
val downloadedTime: String 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)
fileSize.text = fileUtil.convertFileSize(item.format.filesize)
val downloadStartTime = card.findViewById<TextView>(R.id.time)
val time = item.downloadStartTime
if (time == 0L) { if (time == 0L) {
downloadedTime = activity.getString(R.string.currently_downloading) + " " + item.type downloadStartTime.visibility = View.GONE
} else { } else {
downloadStartTime.visibility = View.VISIBLE
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
val date = Date(time * 1000L) val date = Date(time * 1000L)
cal.time = date cal.time = date
@ -100,94 +122,31 @@ class QueuedDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
val year = cal[Calendar.YEAR] val year = cal[Calendar.YEAR]
val formatter: DateFormat = SimpleDateFormat("HH:mm", Locale.getDefault()) val formatter: DateFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
val timeString = formatter.format(date) val timeString = formatter.format(date)
downloadedTime = "$day $month $year - $timeString" downloadStartTime.text = "$day $month $year - $timeString"
} }
datetime.text = downloadedTime
// BUTTON ---------------------------------- // CANCEL BUTTON ----------------------------------
val buttonLayout = card.findViewById<LinearLayout>(R.id.downloads_download_button_layout) val cancelButton = card.findViewById<MaterialButton>(R.id.queued_download_cancel)
val btn = buttonLayout.findViewById<MaterialButton>(R.id.downloads_download_button_type) if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null)
var filePresent = true
//IS IN THE FILE SYSTEM? cancelButton.setOnClickListener {
val path = item.downloadPath onItemClickListener.onQueuedCancelClick(item.id)
val file = File(path)
if (!file.exists() && path.isNotEmpty()) {
filePresent = false
thumbnail.colorFilter = ColorMatrixColorFilter(object : ColorMatrix() {
init {
setSaturation(0f)
}
})
thumbnail.alpha = 0.7f
}
if (item.type == DownloadViewModel.Type.audio) {
if (filePresent) btn.icon = ContextCompat.getDrawable(activity, R.drawable.ic_music_downloaded) else btn.icon = ContextCompat.getDrawable(activity, R.drawable.ic_music)
} else {
if (filePresent) btn.icon = ContextCompat.getDrawable(activity, R.drawable.ic_video_downloaded) else btn.icon = ContextCompat.getDrawable(activity, R.drawable.ic_video)
}
if (btn.hasOnClickListeners()) btn.setOnClickListener(null)
if (checkedItems.contains(item.id)) {
card.isChecked = true
card.strokeWidth = 5
} else {
card.isChecked = false
card.strokeWidth = 0
}
val finalFilePresent = filePresent
card.setOnLongClickListener {
checkCard(card, item.id)
true
}
card.setOnClickListener {
if (checkedItems.size > 0) {
checkCard(card, item.id)
} else {
onItemClickListener.onCardClick(item.id, finalFilePresent)
}
} }
} }
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 { interface OnItemClickListener {
fun onCardClick(itemID: Long, isPresent: Boolean) fun onQueuedCancelClick(itemID: Long)
fun onCardSelect(itemID: Long, isChecked: Boolean)
fun onButtonClick(position: Int)
}
@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()
} }
companion object { companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<HistoryItem> = object : DiffUtil.ItemCallback<HistoryItem>() { private val DIFF_CALLBACK: DiffUtil.ItemCallback<DownloadItem> = object : DiffUtil.ItemCallback<DownloadItem>() {
override fun areItemsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean { override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
val ranged = arrayListOf(oldItem.id, newItem.id) val ranged = arrayListOf(oldItem.id, newItem.id)
return ranged[0] == ranged[1] return ranged[0] == ranged[1]
} }
override fun areContentsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean { override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
return oldItem.time == newItem.time return oldItem.url == newItem.url && oldItem.format.format_id == newItem.format.format_id
} }
} }
} }

View file

@ -0,0 +1,176 @@
package com.deniscerri.ytdlnis.ui
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdlnis.adapter.QueuedDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class DownloadQueueActivity : AppCompatActivity(), ActiveDownloadAdapter.OnItemClickListener, QueuedDownloadAdapter.OnItemClickListener {
private lateinit var activeRecyclerView: RecyclerView
private lateinit var activeDownloads: ActiveDownloadAdapter
private lateinit var queuedRecyclerView: RecyclerView
private lateinit var queuedDownloads: QueuedDownloadAdapter
private lateinit var noResults: RelativeLayout
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var topAppBar: MaterialToolbar
private lateinit var notificationUtil: NotificationUtil
private lateinit var runningLayout: LinearLayout
private lateinit var queuedLayout: LinearLayout
var context: Context? = null
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_download_queue)
context = baseContext
val view : View = window.decorView.findViewById(android.R.id.content)
topAppBar = findViewById(R.id.logs_toolbar)
topAppBar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
activeDownloads =
ActiveDownloadAdapter(
this,
this@DownloadQueueActivity
)
queuedDownloads =
QueuedDownloadAdapter(
this,
this@DownloadQueueActivity
)
activeRecyclerView = findViewById(R.id.active_recyclerview)
activeRecyclerView.layoutManager = LinearLayoutManager(context)
activeRecyclerView.adapter = activeDownloads
queuedRecyclerView = findViewById(R.id.queued_recyclerview)
queuedRecyclerView.layoutManager = LinearLayoutManager(context)
queuedRecyclerView.adapter = queuedDownloads
noResults = findViewById(R.id.no_results)
noResults.visibility = View.GONE
runningLayout = findViewById(R.id.running_layout)
queuedLayout = findViewById(R.id.queued_layout)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
notificationUtil = NotificationUtil(this)
//active downloads livedata
downloadViewModel.activeDownloads.observe(this) {
activeDownloads.submitList(it)
if (it.isEmpty()){
if (queuedLayout.visibility == View.GONE) noResults.visibility = View.VISIBLE
runningLayout.visibility = View.GONE
}else{
noResults.visibility = View.GONE
runningLayout.visibility = View.VISIBLE
it.forEach{item ->
WorkManager.getInstance(this)
.getWorkInfosForUniqueWorkLiveData(item.id.toString())
.observe(this){ list ->
list.forEach {work ->
if (work == null) return@observe
val id = work.progress.getLong("id", 0L)
if(id == 0L) return@observe
val progress = work.progress.getInt("progress", 0)
val output = work.progress.getString("output")
val progressBar = view.findViewWithTag<LinearProgressIndicator>("$id##progress")
val outputText = view.findViewWithTag<TextView>("$id##output")
runOnUiThread {
progressBar.setProgressCompat(progress, true)
outputText.text = output
}
}
}
}
}
}
//queued downloads livedata
downloadViewModel.queuedDownloads.observe(this) {
queuedDownloads.submitList(it)
if (it.isEmpty()){
if (runningLayout.visibility == View.GONE) noResults.visibility = View.VISIBLE
queuedLayout.visibility = View.GONE
}else{
noResults.visibility = View.GONE
queuedLayout.visibility = View.VISIBLE
}
}
}
private fun initMenu(logFolder: File) {
topAppBar.setOnMenuItemClickListener { m: MenuItem ->
val itemId = m.itemId
if (itemId == R.id.remove_logs) {
try{
logFolder.listFiles()!!.forEach {
it.delete()
}
}catch (e: Exception){
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
}
true
}
}
companion object {
private const val TAG = "DownloadQueueActivity"
}
override fun onCancelClick(itemID: Long) {
cancelDownload(itemID)
}
override fun onQueuedCancelClick(itemID: Long) {
lifecycleScope.launch{
withContext(Dispatchers.IO){
downloadViewModel.deleteDownload(downloadViewModel.getItemByID(itemID))
}
}
cancelDownload(itemID)
}
private fun cancelDownload(itemID: Long){
val id = itemID.toInt()
YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(this).cancelUniqueWork(id.toString())
notificationUtil.cancelDownloadNotification(id)
}
}

View file

@ -184,15 +184,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, View.OnClickLi
} }
try { try {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
// DOWNLOAD ALL BUTTON // DOWNLOAD ALL BUTTON
if (resultsList!!.size > 1 || inputQueriesLength > 1) { if (resultsList!!.size > 1 || inputQueriesLength > 1) {
downloadAllFabCoordinator!!.visibility = VISIBLE downloadAllFabCoordinator!!.visibility = VISIBLE
} }
// databaseManager = DatabaseManager(context)
// databaseManager!!.clearResults()
// for (v in resultsList!!) v!!.isPlaylistItem = 1
// databaseManager!!.addToResults(resultsList)
} }
} catch (ignored: Exception) { } catch (ignored: Exception) {
} }
@ -202,14 +197,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, View.OnClickLi
resultViewModel.checkTrending() resultViewModel.checkTrending()
} }
WorkManager.getInstance(requireContext())
.getWorkInfosByTagLiveData("download")
.observe(viewLifecycleOwner){ list ->
list.forEach {
//Toast.makeText(context, """${it.progress.getInt("progress", 0)} ${it.progress.getString("output")}""", Toast.LENGTH_SHORT).show()
}
}
// fragmentView!!.post{ // fragmentView!!.post{
// if (shimmerCards!!.visibility == VISIBLE) return@post // if (shimmerCards!!.visibility == VISIBLE) return@post
// val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager // val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager

View file

@ -209,10 +209,8 @@ class DownloadWorker(
"Format: ${downloadItem.format}\n\n") "Format: ${downloadItem.format}\n\n")
} }
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line ->
setProgressAsync(workDataOf("progress" to progress.toInt())) setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line, "id" to downloadItem.id))
setProgressAsync(workDataOf("output" to line))
val title: String = downloadItem.title val title: String = downloadItem.title
notificationUtil.updateDownloadNotification( notificationUtil.updateDownloadNotification(
downloadItem.id.toInt(), downloadItem.id.toInt(),

View file

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/downloads_card_constraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:id="@+id/active_download_relative_layout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="H,2:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/active_download_card_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="10dp"
app:cardElevation="10dp"
app:cardMaxElevation="12dp"
android:checkable="true"
app:strokeWidth="0dp"
app:cardPreventCornerOverlap="true"
android:layout_margin="10dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/image_view"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:alpha="0.3"
android:scaleY="100"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="10dp"
android:paddingTop="10dp"
android:paddingEnd="10dp"
android:paddingBottom="10dp"
android:shadowColor="@color/black"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="2"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/active_download_cancel"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="bottom"
android:paddingLeft="10dp"
android:paddingTop="10dp"
android:paddingRight="10dp"
android:paddingBottom="10dp"
android:shadowColor="@color/black"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="1.5"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/linearLayout2"
app:layout_constraintEnd_toStartOf="@+id/active_download_cancel"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
<TextView
android:id="@+id/format_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="10dp"
android:paddingStart="10dp"
android:paddingEnd="0dp"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/output"
app:layout_constraintStart_toStartOf="parent" />
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="horizontal"
android:padding="10dp"
app:layout_constraintBottom_toTopOf="@+id/output"
app:layout_constraintStart_toEndOf="@+id/format_note">
<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="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="10dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Tertiary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="10dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
<TextView
android:id="@+id/output"
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingLeft="10dp"
android:paddingTop="10dp"
android:paddingRight="10dp"
android:paddingBottom="10dp"
android:shadowRadius="1.5"
android:textSize="12sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/active_download_cancel"
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Secondary"
android:layout_width="55dp"
android:layout_height="55dp"
android:layout_margin="20dp"
app:cornerRadius="10dp"
app:icon="@drawable/ic_cancel"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.deniscerri.ytdlnis.ui.CustomCommandActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:liftOnScroll="false"
android:background="@android:color/transparent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/logs_toolbar"
android:elevation="0dp"
app:title="@string/command_templates"
android:layout_width="match_parent"
app:navigationIcon="@drawable/ic_back"
android:layout_height="match_parent"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/template_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
>
</androidx.recyclerview.widget.RecyclerView>
<include layout="@layout/history_no_results"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.deniscerri.ytdlnis.ui.CustomCommandActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:liftOnScroll="false"
android:background="@android:color/transparent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/logs_toolbar"
android:elevation="0dp"
app:title="@string/download_queue"
android:layout_width="match_parent"
app:navigationIcon="@drawable/ic_back"
android:layout_height="match_parent"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:scrollbars="none"
android:layout_height="wrap_content">
<LinearLayout
android:padding="10dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/running_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:padding="10dp"
android:textSize="15sp"
android:textStyle="bold"
android:text="@string/running"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/active_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
>
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
<LinearLayout
android:id="@+id/queued_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:padding="10dp"
android:textSize="15sp"
android:textStyle="bold"
android:text="@string/added_to_queue"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/queued_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
>
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<include layout="@layout/history_no_results"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/downloads_card_constraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:id="@+id/queued_download_relative_layout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="H,2.66:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/queued_download_card_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="10dp"
app:cardElevation="10dp"
app:cardMaxElevation="12dp"
android:checkable="true"
app:strokeWidth="0dp"
app:cardPreventCornerOverlap="true"
android:layout_margin="10dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/image_view"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/title"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:paddingStart="10dp"
android:paddingTop="10dp"
android:paddingEnd="10dp"
android:paddingBottom="10dp"
android:shadowColor="@color/black"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="2"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="bottom"
android:paddingLeft="10dp"
android:paddingTop="10dp"
android:paddingRight="10dp"
android:paddingBottom="10dp"
android:shadowColor="@color/black"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="1.5"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
<com.google.android.material.button.MaterialButton
android:layout_margin="15dp"
android:id="@+id/queued_download_cancel"
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Secondary"
android:layout_width="55dp"
android:layout_height="55dp"
app:cornerRadius="10dp"
app:icon="@drawable/ic_baseline_delete_outline_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/format_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/author"
app:layout_constraintVertical_bias="1.0" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="horizontal"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/format_note">
<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="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="10dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Tertiary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="10dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/time"
style="@style/Widget.Material3.FloatingActionButton.Large.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="?attr/colorPrimary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="10dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -4,5 +4,5 @@
<color name="black">#000000</color> <color name="black">#000000</color>
<color name="grey">#323232</color> <color name="grey">#323232</color>
<color name="light_grey">#A6A6A6</color> <color name="light_grey">#A6A6A6</color>
<color name="icon_bg">@android:color/background_light</color> <color name="icon_bg">@android:color/background_dark</color>
</resources> </resources>

View file

@ -181,4 +181,6 @@
<string name="log_downloads">Log Downloads</string> <string name="log_downloads">Log Downloads</string>
<string name="log_downloads_summary">Create a log file for each download</string> <string name="log_downloads_summary">Create a log file for each download</string>
<string name="new_update">New Update</string> <string name="new_update">New Update</string>
<string name="download_queue">Download Queue</string>
<string name="running">Running</string>
</resources> </resources>

View file

@ -41,6 +41,15 @@
<intent android:action="ytdlnis.CommandTemplatesActivity" /> <intent android:action="ytdlnis.CommandTemplatesActivity" />
</Preference> </Preference>
<Preference
app:icon="@drawable/ic_down"
app:key="download_queue"
app:allowDividerBelow="true"
app:title="@string/download_queue">
<intent android:action="ytdlnis.DownloadQueueActivity" />
</Preference>
<Preference <Preference
app:icon="@drawable/ic_settings" app:icon="@drawable/ic_settings"
app:key="open_settings" app:key="open_settings"