mirror of https://github.com/deniscerri/ytdlnis
way too many stuff
- long press download button to save it for later and not schedule it. Also works when u try to queue multiple items - change app icon background based on theme - fixed bug when trying to redownload a history item and wanting to use a different type - made the scheduled item auto-update after its queued so the app doesnt have to update when it begins downloading - some fixes with output templates - fixed container and vcodec being saved with translated strings and not constants - added download thumbnail functionality. Click the result card in the middle and use the video player. Also observe running and queued downloads for that item - removed api key - fixed recommendations sometimes not showing in that location - added feature to disable thumbnails on certain screens. U can choose - added feature to convert subs to different formats - added youtube music search provider - made app name have a color depending on the theme - fixed format card not showing a translated string on best and worst strings - added ability to hide the format intent filter from the share menu - fixed app killing active downloads when removing a queued item - fixed filename template not showing uploader on odysee - other small stuffpull/214/head
parent
f303a4f879
commit
41306565ab
@ -0,0 +1,517 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 9,
|
||||
"identityHash": "bcd20f64b54fbc6e470be0d15059a66e",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "results",
|
||||
"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, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `website` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `formats` TEXT NOT NULL, `urls` TEXT NOT NULL DEFAULT '', `chapters` TEXT, `creationTime` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "author",
|
||||
"columnName": "author",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "duration",
|
||||
"columnName": "duration",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumb",
|
||||
"columnName": "thumb",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "website",
|
||||
"columnName": "website",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "playlistTitle",
|
||||
"columnName": "playlistTitle",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "formats",
|
||||
"columnName": "formats",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "urls",
|
||||
"columnName": "urls",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chapters",
|
||||
"columnName": "chapters",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "creationTime",
|
||||
"columnName": "creationTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "history",
|
||||
"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, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadId` INTEGER NOT NULL DEFAULT 0)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "author",
|
||||
"columnName": "author",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "duration",
|
||||
"columnName": "duration",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumb",
|
||||
"columnName": "thumb",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "downloadPath",
|
||||
"columnName": "downloadPath",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "website",
|
||||
"columnName": "website",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "format",
|
||||
"columnName": "format",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "downloadId",
|
||||
"columnName": "downloadId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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, `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, `workID` BLOB)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "author",
|
||||
"columnName": "author",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumb",
|
||||
"columnName": "thumb",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "duration",
|
||||
"columnName": "duration",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "format",
|
||||
"columnName": "format",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "container",
|
||||
"columnName": "container",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'Default'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "downloadSections",
|
||||
"columnName": "downloadSections",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "allFormats",
|
||||
"columnName": "allFormats",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "downloadPath",
|
||||
"columnName": "downloadPath",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "website",
|
||||
"columnName": "website",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "downloadSize",
|
||||
"columnName": "downloadSize",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "playlistTitle",
|
||||
"columnName": "playlistTitle",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "audioPreferences",
|
||||
"columnName": "audioPreferences",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoPreferences",
|
||||
"columnName": "videoPreferences",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extraCommands",
|
||||
"columnName": "extraCommands",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "customFileNameTemplate",
|
||||
"columnName": "customFileNameTemplate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "SaveThumb",
|
||||
"columnName": "SaveThumb",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'Queued'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "downloadStartTime",
|
||||
"columnName": "downloadStartTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "logID",
|
||||
"columnName": "logID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "workID",
|
||||
"columnName": "workID",
|
||||
"affinity": "BLOB",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "commandTemplates",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "searchHistory",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "query",
|
||||
"columnName": "query",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "templateShortcuts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `content` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "cookies",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `content` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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, `format` TEXT NOT NULL, `downloadType` TEXT NOT NULL, `downloadTime` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"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": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"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, 'bcd20f64b54fbc6e470be0d15059a66e')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
package com.deniscerri.ytdlnis.adapter
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
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.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
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.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import com.squareup.picasso.Picasso
|
||||
import java.lang.StringBuilder
|
||||
|
||||
class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, ActiveDownloadMinifiedAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) {
|
||||
private val onItemClickListener: OnItemClickListener
|
||||
private val activity: Activity
|
||||
private val sharedPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
this.onItemClickListener = onItemClickListener
|
||||
this.activity = activity
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
|
||||
val cardView: MaterialCardView
|
||||
|
||||
init {
|
||||
cardView = itemView.findViewById(R.id.active_download_card_view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val cardView = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.active_download_card_minified, parent, false)
|
||||
return ViewHolder(cardView, onItemClickListener)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
val card = holder.cardView
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.image_view)
|
||||
|
||||
// THUMBNAIL ----------------------------------
|
||||
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue")){
|
||||
val imageURL = item!!.thumb
|
||||
if (imageURL.isNotEmpty()) {
|
||||
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
|
||||
} else {
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
|
||||
}else{
|
||||
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
|
||||
}
|
||||
|
||||
// PROGRESS BAR ----------------------------------------------------
|
||||
val progressBar = card.findViewById<LinearProgressIndicator>(R.id.progress)
|
||||
progressBar.tag = "${item!!.id}##progress"
|
||||
progressBar.progress = 0
|
||||
progressBar.isIndeterminate = true
|
||||
|
||||
// TITLE ----------------------------------
|
||||
val itemTitle = card.findViewById<TextView>(R.id.title)
|
||||
var title = item.title
|
||||
if (title.length > 100) {
|
||||
title = title.substring(0, 40) + "..."
|
||||
}
|
||||
itemTitle.text = title.ifEmpty { item.url }
|
||||
|
||||
val duration = card.findViewById<TextView>(R.id.duration)
|
||||
duration.text = item.duration
|
||||
|
||||
|
||||
val formatDetailsChip = card.findViewById<TextView>(R.id.format_note)
|
||||
val formatDetailsText = StringBuilder(item.format.format_note.uppercase())
|
||||
|
||||
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"){
|
||||
formatDetailsText.append(" \t •\t $codecText")
|
||||
}
|
||||
|
||||
val fileSize = FileUtil.convertFileSize(item.format.filesize)
|
||||
if (fileSize != "?") formatDetailsText.append(" \t •\t $fileSize")
|
||||
|
||||
formatDetailsChip.text = formatDetailsText
|
||||
|
||||
// CANCEL BUTTON ----------------------------------
|
||||
val cancelButton = card.findViewById<MaterialButton>(R.id.active_download_cancel)
|
||||
if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null)
|
||||
|
||||
cancelButton.setOnClickListener {
|
||||
onItemClickListener.onCancelClick(item.id)
|
||||
}
|
||||
|
||||
card.setOnClickListener {
|
||||
onItemClickListener.onCardClick()
|
||||
}
|
||||
}
|
||||
interface OnItemClickListener {
|
||||
fun onCancelClick(itemID: Long)
|
||||
fun onCardClick()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DIFF_CALLBACK: DiffUtil.ItemCallback<DownloadItem> = object : DiffUtil.ItemCallback<DownloadItem>() {
|
||||
override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
|
||||
val ranged = arrayListOf(oldItem.id, newItem.id)
|
||||
return ranged[0] == ranged[1]
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
|
||||
return oldItem.id == newItem.id && oldItem.title == newItem.title && oldItem.author == newItem.author && oldItem.thumb == newItem.thumb
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,560 @@
|
||||
package com.deniscerri.ytdlnis.ui.downloadcard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.format.DateFormat
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.map
|
||||
import androidx.media3.common.MediaItem
|
||||
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.ui.PlayerView
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.work.WorkManager
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.adapter.ActiveDownloadMinifiedAdapter
|
||||
import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.models.ResultItem
|
||||
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.deniscerri.ytdlnis.util.NotificationUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
import com.deniscerri.ytdlnis.util.VideoPlayerUtil
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogFragment(), GenericDownloadAdapter.OnItemClickListener, ActiveDownloadMinifiedAdapter.OnItemClickListener {
|
||||
private lateinit var infoUtil: InfoUtil
|
||||
private lateinit var notificationUtil: NotificationUtil
|
||||
private lateinit var player: ExoPlayer
|
||||
private lateinit var videoView: PlayerView
|
||||
private lateinit var downloadViewModel: DownloadViewModel
|
||||
private lateinit var resultViewModel: ResultViewModel
|
||||
|
||||
private lateinit var activeDownloads: ActiveDownloadMinifiedAdapter
|
||||
private lateinit var queuedDownloads: GenericDownloadAdapter
|
||||
|
||||
private lateinit var queuedItems : List<DownloadItem>
|
||||
private lateinit var activeItems : List<DownloadItem>
|
||||
private lateinit var downloadManager: DownloadManager
|
||||
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
infoUtil = InfoUtil(requireActivity().applicationContext)
|
||||
notificationUtil = NotificationUtil(requireActivity().applicationContext)
|
||||
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
|
||||
queuedItems = listOf()
|
||||
activeItems = listOf()
|
||||
downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
// Inflate the layout to use as dialog or embedded fragment
|
||||
return inflater.inflate(R.layout.result_card_details, container, false)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
return dialog
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("RestrictedApi", "SetTextI18n", "UseGetLayoutInflater")
|
||||
override fun setupDialog(dialog: Dialog, style: Int) {
|
||||
super.setupDialog(dialog, style)
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.result_card_details, null)
|
||||
dialog.setContentView(view)
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
activeDownloads = ActiveDownloadMinifiedAdapter(this,requireActivity())
|
||||
queuedDownloads = GenericDownloadAdapter(this,requireActivity())
|
||||
|
||||
val bottomSheetLink = view.findViewById<MaterialButton>(R.id.bottom_sheet_link)
|
||||
val downloadThumb = view.findViewById<MaterialButton>(R.id.download_thumb)
|
||||
val title = view.findViewById<TextView>(R.id.title)
|
||||
val bottomInfo = view.findViewById<TextView>(R.id.bottom_info)
|
||||
val downloadMusic = view.findViewById<Button>(R.id.download_music)
|
||||
val downloadVideo = view.findViewById<Button>(R.id.download_video)
|
||||
|
||||
val runningRecycler = view.findViewById<RecyclerView>(R.id.running_recycler)
|
||||
val running = view.findViewById<TextView>(R.id.running)
|
||||
val queuedRecycler = view.findViewById<RecyclerView>(R.id.queued_recycler)
|
||||
val queued = view.findViewById<TextView>(R.id.queued)
|
||||
|
||||
runningRecycler.adapter = activeDownloads
|
||||
runningRecycler.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
if (preferences.getBoolean("swipe_gestures", true)){
|
||||
val itemTouchHelper = ItemTouchHelper(simpleCallback)
|
||||
itemTouchHelper.attachToRecyclerView(queuedRecycler)
|
||||
}
|
||||
|
||||
queuedRecycler.adapter = queuedDownloads
|
||||
queuedRecycler.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
|
||||
|
||||
downloadViewModel.activeDownloads.map { it.filter { d -> d.url == item.url } }.observe(viewLifecycleOwner) {
|
||||
if (it.isEmpty()) {
|
||||
running.visibility = View.GONE
|
||||
runningRecycler.visibility = View.GONE
|
||||
}else{
|
||||
running.visibility = View.VISIBLE
|
||||
runningRecycler.visibility = View.VISIBLE
|
||||
|
||||
it.forEach{item ->
|
||||
WorkManager.getInstance(requireContext())
|
||||
.getWorkInfosForUniqueWorkLiveData(item.id.toString())
|
||||
.observe(viewLifecycleOwner){ 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 progressBar = view.findViewWithTag<LinearProgressIndicator>("$id##progress")
|
||||
|
||||
requireActivity().runOnUiThread {
|
||||
try {
|
||||
progressBar?.setProgressCompat(progress, true)
|
||||
}catch (ignored: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeItems = it
|
||||
activeDownloads.submitList(it)
|
||||
}
|
||||
|
||||
|
||||
downloadViewModel.queuedDownloads.map { it.filter { d -> d.url == item.url } }.observe(viewLifecycleOwner) {
|
||||
if (it.isEmpty()) {
|
||||
queued.visibility = View.GONE
|
||||
queuedRecycler.visibility = View.GONE
|
||||
}else{
|
||||
queued.visibility = View.VISIBLE
|
||||
queuedRecycler.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
queuedItems = it
|
||||
queuedDownloads.submitList(it)
|
||||
}
|
||||
|
||||
|
||||
bottomSheetLink.text = item.url
|
||||
bottomSheetLink.setOnClickListener{
|
||||
UiUtil.openLinkIntent(requireContext(), item.url, null)
|
||||
}
|
||||
bottomSheetLink.setOnLongClickListener{
|
||||
UiUtil.copyLinkToClipBoard(requireContext(), item.url, null)
|
||||
true
|
||||
}
|
||||
|
||||
downloadThumb.setOnClickListener {
|
||||
downloadManager.enqueue(
|
||||
DownloadManager.Request(Uri.parse(item.thumb))
|
||||
.setAllowedNetworkTypes(
|
||||
DownloadManager.Request.NETWORK_WIFI or
|
||||
DownloadManager.Request.NETWORK_MOBILE
|
||||
)
|
||||
.setAllowedOverRoaming(true)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setTitle(requireContext().getString(R.string.app_name))
|
||||
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "YTDLnis"))
|
||||
}
|
||||
|
||||
title.text = item.title
|
||||
bottomInfo.text = item.author
|
||||
|
||||
downloadMusic.setOnClickListener {
|
||||
onButtonClick(DownloadViewModel.Type.audio)
|
||||
}
|
||||
|
||||
downloadVideo.setOnClickListener {
|
||||
onButtonClick(DownloadViewModel.Type.video)
|
||||
}
|
||||
|
||||
|
||||
videoView = view.findViewById<PlayerView>(R.id.video_view)
|
||||
val pause = view.findViewById<MaterialButton>(R.id.pause)
|
||||
val frameLayout = view.findViewById<FrameLayout>(R.id.frame_layout)
|
||||
|
||||
val player = VideoPlayerUtil.buildPlayer(requireContext())
|
||||
videoView.player = player
|
||||
|
||||
videoView.setOnClickListener {
|
||||
if (player.isPlaying){
|
||||
player.pause()
|
||||
pause.visibility = View.VISIBLE
|
||||
}
|
||||
else {
|
||||
player.play()
|
||||
pause.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val data : MutableList<String?> = withContext(Dispatchers.IO){
|
||||
if (item.urls.isEmpty()) {
|
||||
infoUtil.getStreamingUrlAndChapters(item.url)
|
||||
}else {
|
||||
item.urls.split("\n").toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
data.removeFirst()
|
||||
|
||||
if (data.isEmpty()) throw Exception("No Streaming URL found!")
|
||||
if (data.size == 2){
|
||||
val audioSource : MediaSource =
|
||||
DefaultMediaSourceFactory(requireContext())
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(data[0])))
|
||||
val videoSource: MediaSource =
|
||||
DefaultMediaSourceFactory(requireContext())
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(data[1])))
|
||||
player.setMediaSource(MergingMediaSource(videoSource, audioSource))
|
||||
}else{
|
||||
player.addMediaItem(MediaItem.fromUri(Uri.parse(data[0])))
|
||||
}
|
||||
player.addMediaItem(MediaItem.fromUri(Uri.parse(data[0])))
|
||||
|
||||
player.prepare()
|
||||
player.play()
|
||||
}catch (e: Exception){
|
||||
frameLayout.visibility = View.GONE
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onButtonClick(type: DownloadViewModel.Type){
|
||||
this.dismiss()
|
||||
if (sharedPreferences.getBoolean("download_card", true)) {
|
||||
val bottomSheet = DownloadBottomSheetDialog(item, type, null, false)
|
||||
bottomSheet.show(parentFragmentManager, "downloadSingleSheet")
|
||||
} else {
|
||||
lifecycleScope.launch{
|
||||
val downloadItem = withContext(Dispatchers.IO){
|
||||
downloadViewModel.createDownloadItemFromResult(item, type)
|
||||
}
|
||||
downloadViewModel.queueDownloads(listOf(downloadItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
super.onCancel(dialog)
|
||||
cleanUp()
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
cleanUp()
|
||||
}
|
||||
|
||||
|
||||
private fun cleanUp(){
|
||||
kotlin.runCatching {
|
||||
videoView.player?.stop()
|
||||
videoView.player?.release()
|
||||
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("resultDetails")!!).commit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeQueuedItem(id: Long){
|
||||
val item = queuedItems.find { it.id == id } ?: return
|
||||
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
|
||||
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
item.status = DownloadRepository.Status.Cancelled.toString()
|
||||
downloadViewModel.updateDownload(item)
|
||||
|
||||
Snackbar.make(requireView().rootView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG)
|
||||
.setAction(getString(R.string.undo)) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
downloadViewModel.deleteDownload(item)
|
||||
downloadViewModel.queueDownloads(listOf(item))
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
deleteDialog.show()
|
||||
}
|
||||
|
||||
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 = queuedItems[position]
|
||||
queuedDownloads.notifyItemChanged(position)
|
||||
removeQueuedItem(deletedItem.id)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.addSwipeRightBackgroundColor(
|
||||
MaterialColors.getColor(
|
||||
requireContext(),
|
||||
R.attr.colorOnSurfaceInverse, Color.TRANSPARENT
|
||||
)
|
||||
)
|
||||
.create()
|
||||
.decorate()
|
||||
super.onChildDraw(
|
||||
c,
|
||||
recyclerView,
|
||||
viewHolder!!,
|
||||
dX,
|
||||
dY,
|
||||
actionState,
|
||||
isCurrentlyActive
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActionButtonClick(itemID: Long) {
|
||||
removeQueuedItem(itemID)
|
||||
}
|
||||
|
||||
override fun onCardClick(itemID: Long) {
|
||||
val item = queuedItems.find { it.id == itemID } ?: return
|
||||
|
||||
val bottomSheet = BottomSheetDialog(requireContext())
|
||||
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet)
|
||||
val title = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)
|
||||
title!!.text = item.title.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
|
||||
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
|
||||
author!!.text = item.author.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
|
||||
|
||||
// BUTTON ----------------------------------
|
||||
val btn = bottomSheet.findViewById<MaterialButton>(R.id.downloads_download_button_type)
|
||||
|
||||
when (item.type) {
|
||||
DownloadViewModel.Type.audio -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_music)
|
||||
}
|
||||
DownloadViewModel.Type.video -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video)
|
||||
}
|
||||
else -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal)
|
||||
}
|
||||
}
|
||||
|
||||
val time = bottomSheet.findViewById<Chip>(R.id.time)
|
||||
val formatNote = bottomSheet.findViewById<Chip>(R.id.format_note)
|
||||
val container = bottomSheet.findViewById<Chip>(R.id.container_chip)
|
||||
val codec = bottomSheet.findViewById<Chip>(R.id.codec)
|
||||
val fileSize = bottomSheet.findViewById<Chip>(R.id.file_size)
|
||||
|
||||
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) time!!.visibility = View.GONE
|
||||
else {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = item.downloadStartTime
|
||||
time!!.text = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(calendar.time)
|
||||
|
||||
time.setOnClickListener {
|
||||
UiUtil.showDatePicker(parentFragmentManager) {
|
||||
bottomSheet.dismiss()
|
||||
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
|
||||
downloadViewModel.deleteDownload(item)
|
||||
item.downloadStartTime = it.timeInMillis
|
||||
WorkManager.getInstance(requireContext()).cancelUniqueWork(item.id.toString())
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(listOf(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
|
||||
View.GONE
|
||||
else formatNote!!.text = item.format.format_note
|
||||
|
||||
if (item.format.container != "") container!!.text = item.format.container.uppercase()
|
||||
else container!!.visibility = View.GONE
|
||||
|
||||
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 fileSizeReadable = FileUtil.convertFileSize(item.format.filesize)
|
||||
if (fileSizeReadable == "?") fileSize!!.visibility = View.GONE
|
||||
else fileSize!!.text = fileSizeReadable
|
||||
|
||||
val link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link)
|
||||
val url = item.url
|
||||
link!!.text = url
|
||||
link.tag = itemID
|
||||
link.setOnClickListener{
|
||||
UiUtil.openLinkIntent(requireContext(), item.url, bottomSheet)
|
||||
}
|
||||
link.setOnLongClickListener{
|
||||
UiUtil.copyLinkToClipBoard(requireContext(), item.url, bottomSheet)
|
||||
true
|
||||
}
|
||||
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
|
||||
remove!!.tag = itemID
|
||||
remove.setOnClickListener{
|
||||
bottomSheet.hide()
|
||||
removeQueuedItem(itemID)
|
||||
}
|
||||
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
|
||||
openFile!!.visibility = View.GONE
|
||||
|
||||
|
||||
|
||||
val downloadNow = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
|
||||
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) downloadNow!!.visibility = View.GONE
|
||||
else{
|
||||
downloadNow!!.text = getString(R.string.download_now)
|
||||
downloadNow.setOnClickListener {
|
||||
bottomSheet.dismiss()
|
||||
downloadViewModel.deleteDownload(item)
|
||||
item.downloadStartTime = 0
|
||||
WorkManager.getInstance(requireContext()).cancelUniqueWork(item.id.toString())
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(listOf(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCardSelect(itemID: Long, isChecked: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun onCancelClick(itemID: Long) {
|
||||
cancelActiveDownload(itemID)
|
||||
}
|
||||
|
||||
override fun onCardClick() {
|
||||
this.dismiss()
|
||||
findNavController().navigate(
|
||||
R.id.downloadQueueMainFragment
|
||||
)
|
||||
}
|
||||
|
||||
private fun cancelActiveDownload(itemID: Long){
|
||||
val id = itemID.toInt()
|
||||
YoutubeDL.getInstance().destroyProcessById(id.toString())
|
||||
WorkManager.getInstance(requireContext()).cancelUniqueWork(id.toString())
|
||||
notificationUtil.cancelDownloadNotification(id)
|
||||
|
||||
activeItems.find { it.id == itemID }?.let {
|
||||
it.status = DownloadRepository.Status.Cancelled.toString()
|
||||
downloadViewModel.updateDownload(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,385 @@
|
||||
package com.deniscerri.ytdlnis.ui.downloads
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
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.chip.Chip
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
|
||||
class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener {
|
||||
private var fragmentView: View? = null
|
||||
private var activity: Activity? = null
|
||||
private lateinit var downloadViewModel : DownloadViewModel
|
||||
private lateinit var savedRecyclerView: RecyclerView
|
||||
private lateinit var savedDownloads: GenericDownloadAdapter
|
||||
private var selectedObjects: ArrayList<DownloadItem>? = null
|
||||
private var actionMode : ActionMode? = null
|
||||
private lateinit var items : MutableList<DownloadItem>
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
|
||||
activity = getActivity()
|
||||
selectedObjects = arrayListOf()
|
||||
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
items = mutableListOf()
|
||||
return fragmentView
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
savedDownloads =
|
||||
GenericDownloadAdapter(
|
||||
this,
|
||||
requireActivity()
|
||||
)
|
||||
|
||||
savedRecyclerView = view.findViewById(R.id.download_recyclerview)
|
||||
savedRecyclerView.adapter = savedDownloads
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
if (preferences.getBoolean("swipe_gestures", true)){
|
||||
val itemTouchHelper = ItemTouchHelper(simpleCallback)
|
||||
itemTouchHelper.attachToRecyclerView(savedRecyclerView)
|
||||
}
|
||||
savedRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
|
||||
|
||||
|
||||
downloadViewModel.savedDownloads.observe(viewLifecycleOwner) {
|
||||
items = it.toMutableList()
|
||||
savedDownloads.submitList(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActionButtonClick(itemID: Long) {
|
||||
val item = items.find { it.id == itemID }
|
||||
if (item != null){
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(listOf(item))
|
||||
}
|
||||
}else{
|
||||
Toast.makeText(requireContext(), getString(R.string.error_restarting_download), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCardClick(itemID: Long) {
|
||||
val item = items.find { it.id == itemID } ?: return
|
||||
|
||||
val bottomSheet = BottomSheetDialog(requireContext())
|
||||
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet)
|
||||
val title = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)
|
||||
title!!.text = item.title.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
|
||||
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
|
||||
author!!.text = item.author.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
|
||||
|
||||
// BUTTON ----------------------------------
|
||||
val btn = bottomSheet.findViewById<MaterialButton>(R.id.downloads_download_button_type)
|
||||
|
||||
when (item.type) {
|
||||
DownloadViewModel.Type.audio -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_music)
|
||||
}
|
||||
DownloadViewModel.Type.video -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video)
|
||||
}
|
||||
else -> {
|
||||
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal)
|
||||
}
|
||||
}
|
||||
|
||||
val time = bottomSheet.findViewById<Chip>(R.id.time)
|
||||
val formatNote = bottomSheet.findViewById<Chip>(R.id.format_note)
|
||||
val container = bottomSheet.findViewById<Chip>(R.id.container_chip)
|
||||
val codec = bottomSheet.findViewById<Chip>(R.id.codec)
|
||||
val fileSize = bottomSheet.findViewById<Chip>(R.id.file_size)
|
||||
|
||||
time!!.visibility = View.GONE
|
||||
|
||||
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
|
||||
View.GONE
|
||||
else formatNote!!.text = item.format.format_note
|
||||
|
||||
if (item.format.container != "") container!!.text = item.format.container.uppercase()
|
||||
else container!!.visibility = View.GONE
|
||||
|
||||
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 fileSizeReadable = FileUtil.convertFileSize(item.format.filesize)
|
||||
if (fileSizeReadable == "?") fileSize!!.visibility = View.GONE
|
||||
else fileSize!!.text = fileSizeReadable
|
||||
|
||||
val link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link)
|
||||
val url = item.url
|
||||
link!!.text = url
|
||||
link.tag = itemID
|
||||
link.setOnClickListener{
|
||||
UiUtil.openLinkIntent(requireContext(), item.url, bottomSheet)
|
||||
}
|
||||
link.setOnLongClickListener{
|
||||
UiUtil.copyLinkToClipBoard(requireContext(), item.url, bottomSheet)
|
||||
true
|
||||
}
|
||||
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
|
||||
remove!!.tag = itemID
|
||||
remove.setOnClickListener{
|
||||
removeItem(item, bottomSheet)
|
||||
}
|
||||
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
|
||||
openFile!!.visibility = View.GONE
|
||||
|
||||
val download = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
|
||||
download!!.text = getString(R.string.download)
|
||||
download.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_downloads, 0, 0, 0);
|
||||
download.tag = itemID
|
||||
download.setOnClickListener{
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(listOf(item))
|
||||
}
|
||||
bottomSheet.cancel()
|
||||
}
|
||||
|
||||
download.setOnLongClickListener {
|
||||
bottomSheet.cancel()
|
||||
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(item), item.type, item, false)
|
||||
sheet.show(parentFragmentManager, "downloadSingleSheet")
|
||||
true
|
||||
}
|
||||
|
||||
openFile.visibility = View.GONE
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCardSelect(itemID: Long, isChecked: Boolean) {
|
||||
val item = items.find { it.id == itemID }
|
||||
if (isChecked) {
|
||||
selectedObjects!!.add(item!!)
|
||||
if (actionMode == null){
|
||||
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
|
||||
|
||||
}else{
|
||||
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
selectedObjects!!.remove(item)
|
||||
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
|
||||
if (selectedObjects!!.isEmpty()){
|
||||
actionMode?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun removeItem(item: DownloadItem, bottomSheet: BottomSheetDialog?){
|
||||
bottomSheet?.hide()
|
||||
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
|
||||
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
downloadViewModel.deleteDownload(item)
|
||||
}
|
||||
deleteDialog.show()
|
||||
}
|
||||
|
||||
|
||||
private val contextualActionBar = object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
mode!!.menuInflater.inflate(R.menu.saved_downloads_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!!){
|
||||
downloadViewModel.deleteDownload(obj)
|
||||
}
|
||||
clearCheckedItems()
|
||||
actionMode?.finish()
|
||||
}
|
||||
deleteDialog.show()
|
||||
true
|
||||
}
|
||||
R.id.download -> {
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(selectedObjects!!.toMutableList())
|
||||
actionMode?.finish()
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.select_all -> {
|
||||
savedDownloads.checkAll(items)
|
||||
selectedObjects?.clear()
|
||||
items.forEach { selectedObjects?.add(it) }
|
||||
mode?.title = getString(R.string.all_items_selected)
|
||||
true
|
||||
}
|
||||
R.id.invert_selected -> {
|
||||
savedDownloads.invertSelected(items)
|
||||
val invertedList = arrayListOf<DownloadItem>()
|
||||
items.forEach {
|
||||
if (!selectedObjects?.contains(it)!!) invertedList.add(it)
|
||||
}
|
||||
selectedObjects?.clear()
|
||||
selectedObjects?.addAll(invertedList)
|
||||
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
|
||||
if (invertedList.isEmpty()) actionMode?.finish()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
actionMode = null
|
||||
clearCheckedItems()
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCheckedItems(){
|
||||
savedDownloads.clearCheckeditems()
|
||||
selectedObjects?.clear()
|
||||
}
|
||||
|
||||
private var simpleCallback: ItemTouchHelper.SimpleCallback =
|
||||
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
|
||||
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]
|
||||
downloadViewModel.deleteDownload(deletedItem)
|
||||
Snackbar.make(savedRecyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
|
||||
.setAction(getString(R.string.undo)) {
|
||||
downloadViewModel.insert(deletedItem)
|
||||
}.show()
|
||||
}
|
||||
ItemTouchHelper.RIGHT -> {
|
||||
onActionButtonClick(items[position].id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.addSwipeRightBackgroundColor(
|
||||
MaterialColors.getColor(
|
||||
requireContext(),
|
||||
R.attr.colorOnSurfaceInverse,Color.TRANSPARENT
|
||||
)
|
||||
)
|
||||
.addSwipeRightActionIcon(R.drawable.ic_downloads)
|
||||
.create()
|
||||
.decorate()
|
||||
super.onChildDraw(
|
||||
c,
|
||||
recyclerView,
|
||||
viewHolder!!,
|
||||
dX,
|
||||
dY,
|
||||
actionState,
|
||||
isCurrentlyActive
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package com.deniscerri.ytdlnis.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
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.trackselection.DefaultTrackSelector
|
||||
import com.deniscerri.ytdlnis.App
|
||||
import org.chromium.net.CronetEngine
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
object VideoPlayerUtil {
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
fun buildPlayer(context: Context) : ExoPlayer {
|
||||
val player: ExoPlayer
|
||||
|
||||
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(context)
|
||||
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(context, cronetDataSourceFactory)
|
||||
|
||||
player = ExoPlayer.Builder(context)
|
||||
.setUsePlatformDiagnostics(false)
|
||||
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
|
||||
.setTrackSelector(trackSelector)
|
||||
.setLoadControl(loadControl)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||
.build()
|
||||
,
|
||||
true)
|
||||
.build()
|
||||
|
||||
return player
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package com.deniscerri.ytdlnis.work
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.navigation.NavDeepLinkBuilder
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.Converters
|
||||
import com.deniscerri.ytdlnis.database.DBManager
|
||||
import com.deniscerri.ytdlnis.database.dao.DownloadDao
|
||||
import com.deniscerri.ytdlnis.database.dao.ResultDao
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
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.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.deniscerri.ytdlnis.util.NotificationUtil
|
||||
import com.deniscerri.ytdlnis.util.UpdateUtil
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
|
||||
|
||||
class UpdatePlaylistFormatsWorker(
|
||||
private val context: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
override suspend fun doWork(): Result {
|
||||
val dbManager = DBManager.getInstance(context)
|
||||
val resultDao = dbManager.resultDao
|
||||
val infoUtil = InfoUtil(context)
|
||||
val converters = Converters()
|
||||
val urls = inputData.getStringArray("urls")!!.toMutableList()
|
||||
|
||||
infoUtil.getFormatsMultiple(urls){
|
||||
val formatStrings = mutableListOf<String>()
|
||||
it.forEach { i -> formatStrings.add(converters.formatToString(i)) }
|
||||
setProgressAsync(workDataOf("formats" to formatStrings.toTypedArray()))
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val result = resultDao.getResultByURL(urls.removeFirst())
|
||||
val formats = infoUtil.getFormats(result.url)
|
||||
result.formats = ArrayList(formats)
|
||||
resultDao.update(result)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.deniscerri.ytdlnis.work
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.navigation.NavDeepLinkBuilder
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.DBManager
|
||||
import com.deniscerri.ytdlnis.database.dao.DownloadDao
|
||||
import com.deniscerri.ytdlnis.database.dao.ResultDao
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
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.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.InfoUtil
|
||||
import com.deniscerri.ytdlnis.util.NotificationUtil
|
||||
import com.deniscerri.ytdlnis.util.UpdateUtil
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
|
||||
|
||||
class UpdateYTDLWorker(
|
||||
private val context: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
override suspend fun doWork(): Result {
|
||||
UpdateUtil(context).updateYoutubeDL()
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:fillAfter="true" >
|
||||
|
||||
<scale
|
||||
android:duration="200"
|
||||
android:fromXScale="0.8"
|
||||
android:fromYScale="0.8"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:toXScale="1"
|
||||
android:toYScale="1" >
|
||||
</scale>
|
||||
|
||||
</set>
|
||||
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?android:colorAccent" android:pathData="M6,17h3l2,-4L11,7L5,7v6h3zM14,17h3l2,-4L19,7h-6v6h3z"/>
|
||||
</vector>
|
||||
@ -0,0 +1,6 @@
|
||||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?android:colorAccent" android:pathData="M21,5c0,-1.1 -0.9,-2 -2,-2H5.83L21,18.17V5z"/>
|
||||
<path android:fillColor="?android:colorAccent" android:pathData="M2.81,2.81L1.39,4.22L3,5.83V19c0,1.1 0.9,2 2,2h13.17l1.61,1.61l1.41,-1.41L2.81,2.81zM6,17l3,-4l2.25,3l0.82,-1.1l2.1,2.1H6z"/>
|
||||
</vector>
|
||||
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M 0 0 H 192 V 192 H 0 V 0 Z" />
|
||||
</vector>
|
||||
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M 0 0 H 192 V 192 H 0 V 0 Z" />
|
||||
</vector>
|
||||
@ -0,0 +1,167 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/active_download_card_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
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"
|
||||
app:cardElevation="10dp"
|
||||
app:cardMaxElevation="12dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
|
||||
<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="200"
|
||||
app:trackColor="#000" />
|
||||
|
||||
<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/image_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="?attr/colorSurfaceVariant"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
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" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:background="#80000000"
|
||||
android:clickable="false"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLength="17"
|
||||
android:maxLines="1"
|
||||
android:minWidth="30dp"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
app:cornerRadius="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/image_view"
|
||||
app:layout_constraintEnd_toEndOf="@+id/image_view" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/download_item_data"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/active_download_cancel"
|
||||
app:layout_constraintHorizontal_weight="0.7"
|
||||
app:layout_constraintStart_toEndOf="@+id/image_view"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:scrollbars="none"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="bottom"
|
||||
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/title">
|
||||
|
||||
<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" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/active_download_cancel"
|
||||
style="?attr/materialIconButtonFilledStyle"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:backgroundTint="?attr/colorSurface"
|
||||
app:cornerRadius="15dp"
|
||||
app:icon="@drawable/ic_cancel"
|
||||
app:iconSize="30dp"
|
||||
app:iconTint="?android:textColorPrimary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="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="download_item_data,active_download_cancel" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@ -0,0 +1,225 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:paddingBottom="20dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/frame_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:cardCornerRadius="0dp"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/video_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:resize_mode="fixed_height"
|
||||
app:useDefaultControls="true"
|
||||
app:use_controller="true" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/download_thumb"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_image"
|
||||
app:iconSize="30dp"
|
||||
app:iconTint="?android:colorAccent"
|
||||
app:layout_constraintTop_toTopOf="@+id/frame_layout"
|
||||
app:layout_constraintEnd_toEndOf="@+id/frame_layout" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/pause"
|
||||
android:visibility="gone"
|
||||
android:clickable="false"
|
||||
app:iconSize="50dp"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
app:iconTint="?android:colorAccent"
|
||||
app:icon="@drawable/exomedia_ic_play_arrow_white"
|
||||
android:layout_width="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:translationZ="10dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:scrollbars="none"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/frame_layout"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/list_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginVertical="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:singleLine="false"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="17sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/download_button_layout"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_info"
|
||||
android:layout_width="0dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="false"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/download_button_layout"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/download_button_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/download_music"
|
||||
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Secondary"
|
||||
android:layout_width="55dp"
|
||||
android:layout_height="55dp"
|
||||
android:layout_marginVertical="5dp"
|
||||
android:layout_marginStart="5dp"
|
||||
app:icon="@drawable/ic_music" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/download_video"
|
||||
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Secondary"
|
||||
android:layout_width="55dp"
|
||||
android:layout_height="55dp"
|
||||
android:layout_margin="5dp"
|
||||
app:icon="@drawable/ic_video" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/title_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="bottom_info,download_button_layout" />
|
||||
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title_barrier">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/running"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:text="@string/running"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/running_recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/running" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/queued"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:text="@string/download_queue"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/running_recycler" />
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/queued_recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/queued" />
|
||||
|
||||
|
||||
|
||||
<Button
|
||||
android:id="@+id/bottom_sheet_link"
|
||||
style="@style/Widget.Material3.Button.TextButton.Icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:singleLine="true"
|
||||
android:text="@string/app_name"
|
||||
android:textAlignment="textStart"
|
||||
android:textSize="15sp"
|
||||
app:icon="@drawable/ic_link"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/queued_recycler" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -0,0 +1,29 @@
|
||||
<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/download"
|
||||
android:title="@string/download"
|
||||
android:icon="@drawable/baseline_download_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>
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background_dark"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background_light"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background_dark"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background_light"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
Loading…
Reference in New Issue