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 stuff
This commit is contained in:
deniscerri 2023-07-15 20:37:28 +02:00
parent f303a4f879
commit 41306565ab
No known key found for this signature in database
GPG key ID: 95C43D517D830350
61 changed files with 2975 additions and 385 deletions

View file

@ -10,7 +10,7 @@ def properties = new Properties()
def versionMajor = 1
def versionMinor = 6
def versionPatch = 3
def versionBuild = 4 // bump for dogfood builds, public betas, etc.
def versionBuild = 5 // bump for dogfood builds, public betas, etc.
def versionExt = ""
if (versionBuild > 0){

View file

@ -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')"
]
}
}

View file

@ -2,6 +2,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@ -31,7 +38,8 @@
android:supportsRtl="true"
android:theme="@style/BaseTheme"
tools:ignore="DataExtractionRules"
tools:targetApi="tiramisu">
tools:targetApi="tiramisu"
android:banner="@mipmap/ic_launcher">
<profileable
android:shell="true"
tools:targetApi="29" />
@ -101,14 +109,71 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity-alias
android:name=".terminalShareAlias"
android:targetActivity=".ui.more.TerminalActivity"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</activity-alias>
<activity-alias
android:name=".Default"
android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:targetActivity=".MainActivity"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".DarkIcon"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_dark"
android:roundIcon="@mipmap/ic_launcher_round_dark"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:targetActivity=".MainActivity"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".LightIcon"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_light"
android:roundIcon="@mipmap/ic_launcher_round_light"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:targetActivity=".MainActivity"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity-alias>
<activity
android:name=".ui.more.WebViewActivity"

View file

@ -5,9 +5,17 @@ import android.widget.Toast
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import androidx.work.Configuration
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.deniscerri.ytdlnis.work.UpdateYTDLWorker
import com.google.android.gms.common.internal.Constants
import com.yausername.aria2c.Aria2c
import com.yausername.ffmpeg.FFmpeg
import com.yausername.youtubedl_android.YoutubeDL
@ -18,6 +26,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class App : Application() {
@ -39,6 +48,23 @@ class App : Application() {
putString("version", BuildConfig.VERSION_NAME)
}
}
//init yt-dlp auto update with work request
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresDeviceIdle(true)
.build()
val periodicWorkRequest = PeriodicWorkRequestBuilder<UpdateYTDLWorker>(1, TimeUnit.DAYS)
.setConstraints(constraints)
.build()
WorkManager.getInstance(this@App).enqueueUniquePeriodicWork(
"ytdlp-Updater",
ExistingPeriodicWorkPolicy.KEEP,
periodicWorkRequest
)
}catch (e: Exception){
Toast.makeText(this@App, e.message, Toast.LENGTH_SHORT).show()
e.printStackTrace()

View file

@ -34,8 +34,11 @@ import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.navOptions
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel

View file

@ -1,6 +1,7 @@
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
@ -9,6 +10,7 @@ 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
@ -29,10 +31,12 @@ import java.lang.StringBuilder
class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, ActiveDownloadAdapter.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) {
@ -52,20 +56,25 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
// THUMBNAIL ----------------------------------
val thumbnail = card.findViewById<ImageView>(R.id.image_view)
val imageURL = item!!.thumb
val uiHandler = Handler(Looper.getMainLooper())
if (imageURL.isNotEmpty()) {
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
} else {
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) }
}
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
// PROGRESS BAR ----------------------------------------------------
val progressBar = card.findViewById<LinearProgressIndicator>(R.id.progress)
progressBar.tag = "${item.id}##progress"
progressBar.tag = "${item!!.id}##progress"
progressBar.progress = 0
progressBar.isIndeterminate = true

View file

@ -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
}
}
}
}

View file

@ -1,6 +1,8 @@
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
@ -9,6 +11,7 @@ import android.view.ViewGroup
import android.widget.FrameLayout
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
@ -24,10 +27,12 @@ import java.util.Locale
class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, ConfigureMultipleDownloadsAdapter.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) {
@ -47,18 +52,25 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
// THUMBNAIL ----------------------------------
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
val imageURL = item!!.thumb
val uiHandler = Handler(Looper.getMainLooper())
if (imageURL.isNotEmpty()) {
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
} else {
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
// THUMBNAIL ----------------------------------
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("home")){
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) }
}
val duration = card.findViewById<TextView>(R.id.duration)
duration.text = item.duration
duration.text = item!!.duration
// TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.title)

View file

@ -2,6 +2,8 @@ package com.deniscerri.ytdlnis.adapter
import android.annotation.SuppressLint
import android.app.Activity
import android.content.SharedPreferences
import android.graphics.Color
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
@ -9,6 +11,7 @@ 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
@ -25,11 +28,13 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
private val onItemClickListener: OnItemClickListener
private val activity: Activity
private val checkedItems: ArrayList<Long>
private val sharedPreferences: SharedPreferences
init {
checkedItems = ArrayList()
this.onItemClickListener = onItemClickListener
this.activity = activity
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
}
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
@ -49,18 +54,23 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
val item = getItem(position)
val card = holder.cardView
// THUMBNAIL ----------------------------------
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
val imageURL = item!!.thumb
val uiHandler = Handler(Looper.getMainLooper())
if (imageURL.isNotEmpty()) {
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
} else {
val thumbnail = card.findViewById<ImageView>(R.id.downloads_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) }
}
}else{
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
}
val duration = card.findViewById<TextView>(R.id.duration)
duration.text = item.duration
duration.text = item!!.duration
// TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.title)
@ -102,6 +112,7 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
when(item.status){
DownloadRepository.Status.Cancelled.toString() -> actionButton.setIconResource(R.drawable.ic_refresh)
DownloadRepository.Status.Saved.toString() -> actionButton.setIconResource(R.drawable.ic_downloads)
DownloadRepository.Status.Queued.toString() -> actionButton.setIconResource(R.drawable.ic_baseline_delete_outline_24)
else -> {
actionButton.setIconResource(R.drawable.ic_baseline_file_open_24)

View file

@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.adapter
import android.annotation.SuppressLint
import android.app.Activity
import android.content.SharedPreferences
import android.graphics.Color
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
@ -13,6 +14,7 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@ -32,11 +34,13 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit
private val checkedItems: ArrayList<Long>
private val onItemClickListener: OnItemClickListener
private val activity: Activity
private val sharedPreferences: SharedPreferences
init {
checkedItems = ArrayList()
this.onItemClickListener = onItemClickListener
this.activity = activity
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
}
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
@ -56,20 +60,26 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
// THUMBNAIL ----------------------------------
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
val imageURL = item!!.thumb
val uiHandler = Handler(Looper.getMainLooper())
if (imageURL.isNotEmpty()) {
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
} else {
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
// THUMBNAIL ----------------------------------
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("downloads")){
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) }
}
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
// TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.downloads_title)
var title = item.title
var title = item!!.title
if (title.length > 100) {
title = title.substring(0, 40) + "..."
}

View file

@ -1,6 +1,7 @@
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
@ -10,6 +11,8 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@ -26,11 +29,13 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
private val checkedVideos: ArrayList<String>
private val onItemClickListener: OnItemClickListener
private val activity: Activity
private val sharedPreferences: SharedPreferences
init {
checkedVideos = ArrayList()
this.onItemClickListener = onItemClickListener
this.activity = activity
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
}
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
@ -50,22 +55,26 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val video = getItem(position)
val card = holder.cardView
// THUMBNAIL ----------------------------------
val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.result_image_view)
val imageURL = video!!.thumb
if (imageURL.isNotEmpty()) {
val uiHandler = Handler(Looper.getMainLooper())
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
// THUMBNAIL ----------------------------------
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("home")){
val imageURL = video!!.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 {
val uiHandler = Handler(Looper.getMainLooper())
}else{
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
thumbnail.setColorFilter(Color.argb(20, 0, 0, 0))
}
// TITLE ----------------------------------
val videoTitle = card.findViewById<TextView>(R.id.result_title)
var title = video.title
var title = video!!.title
if (title.length > 100) {
title = title.substring(0, 40) + "..."
}
@ -145,6 +154,8 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
card.setOnClickListener {
if (checkedVideos.size > 0) {
checkCard(card, videoURL)
}else{
onItemClickListener.onCardDetailsClick(videoURL)
}
}
}
@ -165,6 +176,7 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
fun onButtonClick(videoURL: String, type: DownloadViewModel.Type?)
fun onLongButtonClick(videoURL: String, type: DownloadViewModel.Type?)
fun onCardClick(videoURL: String, add: Boolean)
fun onCardDetailsClick(videoURL: String)
}
fun checkAll(items: List<ResultItem?>?){

View file

@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.adapter
import android.annotation.SuppressLint
import android.app.Activity
import android.content.SharedPreferences
import android.graphics.Color
import android.os.Handler
import android.os.Looper
@ -12,6 +13,7 @@ import android.widget.CheckBox
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@ -26,11 +28,13 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
private val checkedItems: ArrayList<String>
private val onItemClickListener: OnItemClickListener
private val activity: Activity
private val sharedPreferences: SharedPreferences
init {
checkedItems = ArrayList()
this.onItemClickListener = onItemClickListener
this.activity = activity
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
}
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
@ -50,20 +54,25 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
// THUMBNAIL ----------------------------------
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
val imageURL = item!!.thumb
val uiHandler = Handler(Looper.getMainLooper())
if (imageURL.isNotEmpty()) {
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
} else {
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
// THUMBNAIL ----------------------------------
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("home")){
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(95, 0, 0, 0))
}else{
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }
}
thumbnail.setColorFilter(Color.argb(95, 0, 0, 0))
// TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.title)
var title = item.title
var title = item!!.title
if (title.length > 100) {
title = title.substring(0, 40) + "..."
}

View file

@ -40,8 +40,8 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Processing' ORDER BY id DESC")
fun getProcessingDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC")
fun getSavedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE id=:id LIMIT 1")
fun getDownloadById(id: Long) : DownloadItem
@ -70,6 +70,9 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE status='Error'")
suspend fun deleteErrored()
@Query("DELETE FROM downloads WHERE status='Saved'")
suspend fun deleteSaved()
@Query("UPDATE downloads SET status='Cancelled' WHERE status='Queued'")
suspend fun cancelQueued()

View file

@ -13,10 +13,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
val queuedDownloads : Flow<List<DownloadItem>> = downloadDao.getQueuedDownloads()
val cancelledDownloads : Flow<List<DownloadItem>> = downloadDao.getCancelledDownloads()
val erroredDownloads : Flow<List<DownloadItem>> = downloadDao.getErroredDownloads()
val processingDownloads : Flow<List<DownloadItem>> = downloadDao.getProcessingDownloads()
val savedDownloads : Flow<List<DownloadItem>> = downloadDao.getSavedDownloads()
enum class Status {
Active, Queued, Error, Processing, Cancelled
Active, Queued, Error, Processing, Cancelled, Saved
}
suspend fun insert(item: DownloadItem) : Long {
@ -73,6 +73,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.deleteErrored()
}
suspend fun deleteSaved(){
downloadDao.deleteSaved()
}
suspend fun cancelQueued(){
downloadDao.cancelQueued()
}

View file

@ -24,7 +24,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
suspend fun updateTrending(){
deleteAll()
val infoUtil = InfoUtil(context)
val items = infoUtil.getTrending(context)
val items = infoUtil.getTrending()
itemCount.value = items.size
for (i in items){
resultDao.insert(i!!)

View file

@ -20,10 +20,13 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.AudioPreferences
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.DownloadItem
@ -40,13 +43,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.runBlocking
import java.io.File
import java.util.Locale
import java.util.concurrent.TimeUnit
class DownloadViewModel(application: Application) : AndroidViewModel(application) {
private val dbManager: DBManager
private val repository : DownloadRepository
private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao
@ -57,7 +61,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val activeDownloadsCount : LiveData<Int>
val cancelledDownloads : LiveData<List<DownloadItem>>
val erroredDownloads : LiveData<List<DownloadItem>>
val processingDownloads : LiveData<List<DownloadItem>>
val savedDownloads : LiveData<List<DownloadItem>>
private var bestVideoFormat : Format
private var bestAudioFormat : Format
@ -72,7 +76,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
init {
val dao = DBManager.getInstance(application).downloadDao
dbManager = DBManager.getInstance(application)
val dao = dbManager.downloadDao
repository = DownloadRepository(dao)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
@ -82,7 +87,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
queuedDownloads = repository.queuedDownloads.asLiveData()
activeDownloads = repository.activeDownloads.asLiveData()
activeDownloadsCount = repository.activeDownloadsCount.asLiveData()
processingDownloads = repository.processingDownloads.asLiveData()
savedDownloads = repository.savedDownloads.asLiveData()
cancelledDownloads = repository.cancelledDownloads.asLiveData()
erroredDownloads = repository.erroredDownloads.asLiveData()
@ -97,8 +102,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val videoFormatValues = resources.getStringArray(R.array.video_formats_values)
var videoContainer = sharedPreferences.getString("video_format", "Default")
if (videoContainer == "Default") videoContainer = App.instance.getString(R.string.defaultValue)
val videoContainer = sharedPreferences.getString("video_format", "Default")
defaultVideoFormats = mutableListOf()
videoFormatValues.forEach {
@ -116,8 +120,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
bestVideoFormat = defaultVideoFormats.last()
var audioContainer = sharedPreferences.getString("audio_format", "mp3")
if (audioContainer == "Default") audioContainer = App.instance.getString(R.string.defaultValue)
val audioContainer = sharedPreferences.getString("audio_format", "mp3")
bestAudioFormat = Format(
"best",
audioContainer!!,
@ -374,8 +377,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
fun getGenericAudioFormats() : MutableList<Format>{
val audioFormats = resources.getStringArray(R.array.audio_formats)
val formats = mutableListOf<Format>()
var containerPreference = sharedPreferences.getString("audio_format", "Default")
if (containerPreference == "Default") containerPreference = resources.getString(R.string.defaultValue)
val containerPreference = sharedPreferences.getString("audio_format", "")
audioFormats.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) }
return formats
}
@ -383,9 +385,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
fun getGenericVideoFormats() : MutableList<Format>{
val videoFormats = resources.getStringArray(R.array.video_formats_values)
val formats = mutableListOf<Format>()
var containerPreference = sharedPreferences.getString("video_format", "Default")
if (containerPreference == "Default") containerPreference = resources.getString(R.string.defaultValue)
videoFormats.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.defaultValue),"", "",0, it)) }
val containerPreference = sharedPreferences.getString("video_format", "")
videoFormats.forEach { formats.add(Format(it, containerPreference!!,"Default","", "",0, it)) }
return formats
}
@ -415,6 +416,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
repository.deleteErrored()
}
fun deleteSaved() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteSaved()
}
fun cancelQueued() = viewModelScope.launch(Dispatchers.IO) {
repository.cancelQueued()
}
@ -450,6 +455,9 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val queuedItems = mutableListOf<DownloadItem>()
var lastDownloadId = repository.getLastDownloadId()
var exists = false
items.forEach {
lastDownloadId++
it.status = DownloadRepository.Status.Queued.toString()
@ -458,9 +466,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
} != null) {
Looper.prepare().run {
Toast.makeText(context, context.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
}
exists = true
}else{
if (it.id == 0L){
it.id = lastDownloadId
@ -475,6 +481,12 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
}
if (exists){
Looper.prepare().run {
Toast.makeText(context, context.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
}
}
queuedItems.forEach {
val currentTime = System.currentTimeMillis()
var delay = if (it.downloadStartTime != 0L){
@ -498,12 +510,6 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
workRequest
).enqueue()
it.id = withContext(Dispatchers.IO){
repository.checkIfReDownloadingErroredOrCancelled(it)
}
if (it.id != 0L){
repository.delete(it)
}
}
val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered
@ -512,6 +518,35 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show()
}
}
items.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
try{
updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao)
}catch (ignored: Exception){}
}
}
private fun updateDownloadItem(
downloadItem: DownloadItem,
infoUtil: InfoUtil,
dao: DownloadDao,
resultDao: ResultDao
) : Boolean {
var wasQuickDownloaded = false
if (downloadItem.title.isEmpty() || downloadItem.author.isEmpty() || downloadItem.thumb.isEmpty()){
runCatching {
val info = infoUtil.getMissingInfo(downloadItem.url)
if (downloadItem.title.isEmpty()) downloadItem.title = info?.title.toString()
if (downloadItem.author.isEmpty()) downloadItem.author = info?.author.toString()
downloadItem.duration = info?.duration.toString()
downloadItem.website = info?.website.toString()
if (downloadItem.thumb.isEmpty()) downloadItem.thumb = info?.thumb.toString()
runBlocking {
wasQuickDownloaded = resultDao.getCountInt() == 0
dao.update(downloadItem)
}
}
}
return wasQuickDownloaded
}
}

View file

@ -5,12 +5,14 @@ import android.app.Activity
import android.content.*
import android.content.Context.CLIPBOARD_SERVICE
import android.content.res.ColorStateList
import android.content.res.Resources.Theme
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.util.Log
import android.util.TypedValue
import android.view.*
import android.view.View.*
import android.widget.*
@ -37,7 +39,9 @@ import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadMultipleBottomSheetDialog
import com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.ThemeUtil
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
@ -133,6 +137,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
searchHistory = view.findViewById(R.id.search_history_scroll_view)
searchHistoryLinearLayout = view.findViewById(R.id.search_history_linear_layout)
materialToolbar!!.title = ThemeUtil.getStyledAppName(requireContext())
homeAdapter =
HomeAdapter(
this,
@ -567,6 +573,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}
override fun onCardDetailsClick(videoURL: String) {
if (parentFragmentManager.findFragmentByTag("resultDetails") == null && resultsList != null && resultsList!!.isNotEmpty()){
val bottomSheet = ResultCardDetailsDialog(resultsList!!.first{it!!.url == videoURL}!!)
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
}
}
override fun onClick(v: View) {
val viewIdName: String = try {
v.tag.toString()

View file

@ -32,6 +32,7 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.VideoPlayerUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
@ -104,64 +105,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
behavior.peekHeight = displayMetrics.heightPixels
}
// val renderersFactory = DefaultRenderersFactory(requireContext().applicationContext)
// .setEnableDecoderFallback(true)
// .setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER)
// .setMediaCodecSelector { mimeType, requiresSecureDecoder, requiresTunnelingDecoder ->
// var decoderInfo: MutableList<MediaCodecInfo> =
// MediaCodecSelector.DEFAULT
// .getDecoderInfos(
// mimeType,
// requiresSecureDecoder,
// requiresTunnelingDecoder
// )
// if (MimeTypes.VIDEO_H264 == mimeType) {
// // copy the list because MediaCodecSelector.DEFAULT returns an unmodifiable list
// decoderInfo = ArrayList(decoderInfo)
// decoderInfo.reverse()
// }
// decoderInfo
// }
val cronetEngine: CronetEngine = CronetEngine.Builder(App.instance)
.enableHttp2(true)
.enableQuic(true)
.enableBrotli(true)
.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, 1024L * 1024L) // 1MiB
.build()
val trackSelector = DefaultTrackSelector(requireContext())
val loadControl = DefaultLoadControl.Builder()
// cache the last three minutes
.setBackBuffer(1000 * 60 * 3, true)
.setBufferDurationsMs(
1000 * 10, // exo default is 50s
50000,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
)
.build()
val cronetDataSourceFactory = CronetDataSource.Factory(
cronetEngine,
Executors.newCachedThreadPool()
)
val dataSourceFactory = DefaultDataSource.Factory(requireContext(), cronetDataSourceFactory)
player = ExoPlayer.Builder(requireContext())
.setUsePlatformDiagnostics(false)
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
.setTrackSelector(trackSelector)
.setLoadControl(loadControl)
.setHandleAudioBecomingNoisy(true)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build()
,
true)
.build()
player = VideoPlayerUtil.buildPlayer(requireContext())
val frame = view.findViewById<MaterialCardView>(R.id.frame_layout)
val videoView = view.findViewById<PlayerView>(R.id.video_view)

View file

@ -142,9 +142,13 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
//if its updating a present downloaditem and its the wrong category
if (currentDownloadItem!!.type != Type.audio){
downloadItem.type = Type.audio
downloadItem.format =
downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
.maxByOrNull { it.filesize }!!
runCatching {
downloadItem.format =
downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
.maxByOrNull { it.filesize }!!
}.onFailure {
downloadItem.format = downloadViewModel.getGenericAudioFormats().last()
}
}
}
if (formats.isEmpty()) formats.addAll(downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) })
@ -160,11 +164,11 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
val formatCard = view.findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
val chosenFormat = downloadItem.format
UiUtil.populateFormatCard(formatCard, chosenFormat, null)
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null)
val listener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, item: List<FormatTuple>) {
downloadItem.format = item.first().format
UiUtil.populateFormatCard(formatCard, item.first().format, null)
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, null)
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultItem.formats.removeAll(formats.toSet())

View file

@ -23,6 +23,7 @@ import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
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.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
@ -33,6 +34,7 @@ import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.*
import java.text.SimpleDateFormat
@ -201,6 +203,22 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
dismiss()
}
download.setOnLongClickListener {
val dd = MaterialAlertDialogBuilder(requireContext())
dd.setTitle(getString(R.string.save_for_later))
dd.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
dd.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
lifecycleScope.launch(Dispatchers.IO){
val item = getDownloadItem()
item.status = DownloadRepository.Status.Saved.toString()
downloadViewModel.updateDownload(item)
dismiss()
}
}
dd.show()
true
}
val link = view.findViewById<Button>(R.id.bottom_sheet_link)
link.text = resultItem.url
link.setOnClickListener{

View file

@ -38,6 +38,7 @@ import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
@ -139,6 +140,23 @@ class DownloadMultipleBottomSheetDialog(private var results: List<ResultItem?>,
dismiss()
}
download.setOnLongClickListener {
val dd = MaterialAlertDialogBuilder(requireContext())
dd.setTitle(getString(R.string.save_for_later))
dd.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
dd.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
lifecycleScope.launch(Dispatchers.IO){
items.forEach {
it.status = DownloadRepository.Status.Saved.toString()
downloadViewModel.updateDownload(it)
}
dismiss()
}
}
dd.show()
true
}
bottomAppBar = view.findViewById(R.id.bottomAppBar)
val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type)
when(items.first().type){

View file

@ -147,9 +147,13 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
//if its updating a present downloaditem and its the wrong category
if (currentDownloadItem!!.type != Type.video){
downloadItem.type = Type.video
downloadItem.format =
downloadItem.allFormats.filter { it.vcodec.isNotEmpty() }
.maxByOrNull { it.filesize }!!
runCatching {
downloadItem.format =
downloadItem.allFormats.filter { it.vcodec.isNotEmpty() }
.maxByOrNull { it.filesize }!!
}.onFailure {
downloadItem.format = downloadViewModel.getGenericVideoFormats().last()
}
}
}
if (formats.isEmpty()) formats.addAll(downloadItem.allFormats)
@ -166,7 +170,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
val formatCard = view.findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
val chosenFormat = downloadItem.format
UiUtil.populateFormatCard(formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) })
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) })
val listener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, item: List<FormatTuple>) {
downloadItem.format = item.first().format
@ -180,7 +184,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
}
}
formats = allFormats.first().toMutableList()
UiUtil.populateFormatCard(formatCard, item.first().format, item.first().audioFormats)
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, item.first().audioFormats)
}
}
formatCard.setOnClickListener{

View file

@ -253,7 +253,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
val format = finalFormats[i]
val formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null)
formatItem.tag = "${format.format_id}${format.format_note}"
UiUtil.populateFormatCard(formatItem as MaterialCardView, format, null)
UiUtil.populateFormatCard(requireContext(), formatItem as MaterialCardView, format, null)
formatItem.setOnClickListener{ clickedformat ->
//if the context is behind a video or playlist, allow the ability to multiselect audio formats
if (canMultiSelectAudio){

View file

@ -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)
}
}
}

View file

@ -63,7 +63,7 @@ class DownloadQueueMainFragment : Fragment(){
overScrollMode = View.OVER_SCROLL_NEVER
}
val fragments = mutableListOf(ActiveDownloadsFragment(), QueuedDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment())
val fragments = mutableListOf(ActiveDownloadsFragment(), QueuedDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment(), SavedDownloadsFragment())
fragmentAdapter = DownloadListFragmentAdapter(
childFragmentManager,
@ -80,6 +80,7 @@ class DownloadQueueMainFragment : Fragment(){
1 -> tab.text = getString(R.string.in_queue)
2 -> tab.text = getString(R.string.cancelled)
3 -> tab.text = getString(R.string.errored)
4 -> tab.text = getString(R.string.saved)
}
}.attach()
@ -113,21 +114,26 @@ class DownloadQueueMainFragment : Fragment(){
try{
when(m.itemId){
R.id.clear_queue -> {
showDeleteDialog() {
showDeleteDialog {
cancelAllDownloads()
downloadViewModel.cancelQueued()
}
}
R.id.clear_cancelled -> {
showDeleteDialog() {
showDeleteDialog {
downloadViewModel.deleteCancelled()
}
}
R.id.clear_errored -> {
showDeleteDialog() {
showDeleteDialog {
downloadViewModel.deleteErrored()
}
}
R.id.clear_saved -> {
showDeleteDialog {
downloadViewModel.deleteSaved()
}
}
}
}catch (e: Exception){
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()

View file

@ -90,6 +90,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
private var _binding : FragmentHistoryBinding? = null
private var actionMode : ActionMode? = null
private lateinit var sortChip: Chip
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@ -111,6 +113,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
selectionChips = view.findViewById(R.id.history_selection_chips)
websiteGroup = view.findViewById(R.id.website_chip_group)
uiHandler = Handler(Looper.getMainLooper())
sortChip = view.findViewById(R.id.sortChip)
selectedObjects = ArrayList()
@ -155,6 +158,27 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
scrollToTop()
}
historyViewModel.sortOrder.observe(viewLifecycleOwner){
if (it != null){
when(it){
HistorySort.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down)
HistorySort.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up)
}
}
}
historyViewModel.sortType.observe(viewLifecycleOwner){
if(it != null){
when(it){
HistoryRepository.HistorySortType.AUTHOR -> sortChip.text = getString(R.string.author)
HistoryRepository.HistorySortType.DATE -> sortChip.text = getString(R.string.date_added)
HistoryRepository.HistorySortType.TITLE -> sortChip.text = getString(R.string.title)
HistoryRepository.HistorySortType.FILESIZE -> sortChip.text = getString(R.string.file_size)
}
}
}
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
initMenu()
initChips()

View file

@ -51,6 +51,7 @@ import kotlinx.coroutines.runBlocking
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.UUID
class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener {
@ -102,9 +103,13 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
items = it.toMutableList()
if (it.isEmpty()) fileSize.visibility = View.GONE
else{
fileSize.visibility = View.VISIBLE
fileSize.text = "${getString(R.string.file_size)}: ~ ${FileUtil.convertFileSize(it.sumOf { i -> i.format.filesize })}"
}
val size = FileUtil.convertFileSize(it.sumOf { i -> i.format.filesize })
if (size == "?") fileSize.visibility = View.GONE
else {
fileSize.visibility = View.VISIBLE
fileSize.text = "${getString(R.string.file_size)}: ~ $size"
}
}
queuedDownloads.submitList(it)
}
}
@ -262,17 +267,13 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
}
}
private fun removeItem(itemID: Long){
val item = items.find { it.id == itemID }
private fun removeItem(id: Long){
val item = items.find { it.id == id } ?: return
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item?.title + "\"!")
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 ->
val id = itemID.toInt()
YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(requireContext()).cancelUniqueWork(id.toString())
notificationUtil.cancelDownloadNotification(id)
item!!.status = DownloadRepository.Status.Cancelled.toString()
item.status = DownloadRepository.Status.Cancelled.toString()
downloadViewModel.updateDownload(item)
Snackbar.make(queuedRecyclerView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG)

View file

@ -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
)
}
}
}

View file

@ -1,6 +1,9 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.ListView
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference

View file

@ -1,6 +1,10 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.content.ComponentName
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
@ -14,9 +18,10 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.ui.more.TerminalActivity
import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.gms.common.wrappers.Wrappers.packageManager
import java.util.Locale
@ -28,6 +33,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
private var accent: ListPreference? = null
private var highContrast: SwitchPreferenceCompat? = null
private var locale: ListPreference? = null
private var hideTerminal: SwitchPreferenceCompat? = null
private var updateUtil: UpdateUtil? = null
private var activeDownloadCount = 0
@ -48,6 +54,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
accent = findPreference("theme_accent")
highContrast = findPreference("high_contrast")
locale = findPreference("locale")
hideTerminal = findPreference("hide_terminal")
if(VERSION.SDK_INT < VERSION_CODES.TIRAMISU){
val values = resources.getStringArray(R.array.language_values)
@ -108,5 +115,21 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
true
}
hideTerminal!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { pref: Preference?, _: Any ->
val packageManager = requireContext().packageManager
val aliasComponentName = ComponentName(requireContext(), "com.deniscerri.ytdlnis.terminalShareAlias")
if ((pref as SwitchPreferenceCompat).isChecked){
packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP)
}else{
packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP)
}
true
}
}
}

View file

@ -64,7 +64,6 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
private lateinit var cookieViewModel: CookieViewModel
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private var arch: Preference? = null
private var version: Preference? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -201,9 +200,8 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
version = findPreference("version")
val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir
val installedABI = Build.SUPPORTED_ABIS.first { nativeLibraryDir!!.contains(it) }
version!!.summary = "${BuildConfig.VERSION_NAME} [${installedABI}] ${BuildConfig.BUILD_TYPE}"
version!!.summary = "${BuildConfig.VERSION_NAME} [${nativeLibraryDir?.split("/lib/")?.get(1)}] ${BuildConfig.BUILD_TYPE}"
version!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {

View file

@ -34,12 +34,10 @@ import java.util.regex.Pattern
class InfoUtil(private val context: Context) {
private var items: ArrayList<ResultItem?>
private lateinit var sharedPreferences: SharedPreferences
private var key: String? = null
init {
try {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
key = sharedPreferences.getString("api_key", "")
countryCODE = sharedPreferences.getString("locale", "")!!
if (countryCODE.isEmpty()) countryCODE = "US"
} catch (e: Exception) {
@ -51,70 +49,44 @@ class InfoUtil(private val context: Context) {
fun search(query: String): ArrayList<ResultItem?> {
items = ArrayList()
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
return if (searchEngine == "ytsearch"){
if (key!!.isNotEmpty()) searchFromKey(query) else {
try{
searchFromPiped(query)
}catch (e: Exception){
getFromYTDL(query)
}
return when(sharedPreferences.getString("search_engine", "ytsearch")){
"ytsearch" -> try{
searchFromPiped(query)
}catch (e: Exception){
getFromYTDL(query)
}
}else getFromYTDL(query)
"ytsearchmusic" -> try{
searchFromPipedMusic(query)
}catch (e: Exception){
getFromYTDL(query)
}
else -> getFromYTDL(query)
}
}
@Throws(JSONException::class)
fun searchFromKey(query: String): ArrayList<ResultItem?> {
//short data
val res =
genericRequest("https://youtube.googleapis.com/youtube/v3/search?part=snippet&q=$query&maxResults=25&regionCode=$countryCODE&key=$key")
if (!res.has("items")) return getFromYTDL(query)
val dataArray = res.getJSONArray("items")
//extra data
var url2 = "https://www.googleapis.com/youtube/v3/videos?id="
//getting all ids, for the extra data request
fun searchFromPiped(query: String): ArrayList<ResultItem?> {
val data = genericRequest("$pipedURL/search?q=$query&filter=videos")
val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return getFromYTDL(query)
for (i in 0 until dataArray.length()) {
val element = dataArray.getJSONObject(i)
val snippet = element.getJSONObject("snippet")
if (element.getJSONObject("id").getString("kind") == "youtube#video") {
val videoID = element.getJSONObject("id").getString("videoId")
url2 = "$url2$videoID,"
snippet.put("videoID", videoID)
}
}
url2 = url2.substring(
0,
url2.length - 1
) + "&part=contentDetails&regionCode=" + countryCODE + "&key=" + key
val extra = genericRequest(url2)
var j = 0
for (i in 0 until dataArray.length()) {
val element = dataArray.getJSONObject(i)
val snippet = element.getJSONObject("snippet")
if (element.getJSONObject("id").getString("kind") == "youtube#video") {
var duration =
extra.getJSONArray("items").getJSONObject(j++).getJSONObject("contentDetails")
.getString("duration")
duration = formatDuration(duration)
if (duration == "0:00") {
continue
}
snippet.put("duration", duration)
fixThumbnail(snippet)
val v = createVideofromJSON(snippet)
if (v == null || v.thumb.isEmpty()) {
continue
}
items.add(createVideofromJSON(snippet))
if (element.getInt("duration") == -1) continue
element.put("uploader", element.getString("uploaderName"))
val v = createVideoFromPipedJSON(element, element.getString("url").removePrefix("/watch?v="))
if (v == null || v.thumb.isEmpty()) {
continue
}
items.add(v)
}
return items
}
@Throws(JSONException::class)
fun searchFromPiped(query: String): ArrayList<ResultItem?> {
val data = genericRequest("$pipedURL/search?q=$query?filter=videos")
fun searchFromPipedMusic(query: String): ArrayList<ResultItem?> {
val data = genericRequest("$pipedURL/search?q=$query=&filter=music_songs")
val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return getFromYTDL(query)
for (i in 0 until dataArray.length()) {
@ -134,77 +106,24 @@ class InfoUtil(private val context: Context) {
fun getPlaylist(id: String, nextPageToken: String): PlaylistTuple {
try{
items = ArrayList()
if (key!!.isEmpty()) {
// -------------- PIPED API FUNCTION -------------------
var url = ""
url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id"
else """$pipedURL/nextpage/playlists/$id?nextpage=${nextPageToken.replace("&prettyPrint", "%26prettyPrint")}"""
// -------------- PIPED API FUNCTION -------------------
var url = ""
url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id"
else """$pipedURL/nextpage/playlists/$id?nextpage=${nextPageToken.replace("&prettyPrint", "%26prettyPrint")}"""
val res = genericRequest(url)
if (!res.has("relatedStreams")) throw Exception()
val dataArray = res.getJSONArray("relatedStreams")
var nextpage = res.getString("nextpage")
for (i in 0 until dataArray.length()){
val obj = dataArray.getJSONObject(i)
val itm = createVideoFromPipedJSON(obj, obj.getString("url").removePrefix("/watch?v="))
itm?.playlistTitle = "YTDLnis"
items.add(itm)
}
if (nextpage == "null") nextpage = ""
return PlaylistTuple(nextpage, items)
}
//---------- YOUTUBE API FUNCTION --------------------------------
val url = "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet&pageToken=$nextPageToken&maxResults=50&regionCode=$countryCODE&playlistId=$id&key=$key"
//short data
val res = genericRequest(url)
if (!res.has("items")) return PlaylistTuple(
"",
getFromYTDL("https://www.youtube.com/playlist?list=$id")
)
val dataArray = res.getJSONArray("items")
if (!res.has("relatedStreams")) throw Exception()
//extra data
var url2 = "https://www.googleapis.com/youtube/v3/videos?id="
//getting all ids, for the extra data request
for (i in 0 until dataArray.length()) {
val element = dataArray.getJSONObject(i)
val snippet = element.getJSONObject("snippet")
val videoID = snippet.getJSONObject("resourceId").getString("videoId")
url2 = "$url2$videoID,"
snippet.put("videoID", videoID)
val dataArray = res.getJSONArray("relatedStreams")
var nextpage = res.getString("nextpage")
for (i in 0 until dataArray.length()){
val obj = dataArray.getJSONObject(i)
val itm = createVideoFromPipedJSON(obj, obj.getString("url").removePrefix("/watch?v="))
itm?.playlistTitle = "YTDLnis"
items.add(itm)
}
url2 = url2.substring(
0,
url2.length - 1
) + "&part=contentDetails&regionCode=" + countryCODE + "&key=" + key
val extra = genericRequest(url2)
val extraArray = extra.getJSONArray("items")
var j = 0
var i = 0
while (i < extraArray.length()) {
val element = dataArray.getJSONObject(i)
val snippet = element.getJSONObject("snippet")
var duration =
extra.getJSONArray("items").getJSONObject(j).getJSONObject("contentDetails")
.getString("duration")
duration = formatDuration(duration)
snippet.put("duration", duration)
fixThumbnail(snippet)
val v = createVideofromJSON(snippet)
if (v == null || v.thumb.isEmpty()) {
i++
continue
} else {
j++
}
v.playlistTitle = "YTDLnis"
items.add(v)
i++
}
val next = res.optString("nextPageToken")
return PlaylistTuple(next, items)
if (nextpage == "null") nextpage = ""
return PlaylistTuple(nextpage, items)
}catch (e: Exception){
return PlaylistTuple(
"",
@ -215,29 +134,14 @@ class InfoUtil(private val context: Context) {
@Throws(JSONException::class)
fun getVideo(url: String): List<ResultItem?> {
try {
return try {
val id = getIDFromYoutubeURL(url)
if (key!!.isEmpty()) {
val res = genericRequest("$pipedURL/streams/$id")
return if (res.length() == 0) getFromYTDL(url) else listOf(createVideoFromPipedJSON(
res, id
))
}
//short data
var res =
genericRequest("https://youtube.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=$id&key=$key")
if (!res.has("items")) return getFromYTDL(url)
var duration = res.getJSONArray("items").getJSONObject(0).getJSONObject("contentDetails")
.getString("duration")
duration = formatDuration(duration)
res = res.getJSONArray("items").getJSONObject(0).getJSONObject("snippet")
res.put("videoID", id)
res.put("duration", duration)
fixThumbnail(res)
return listOf(createVideofromJSON(res))
val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) getFromYTDL(url) else listOf(createVideoFromPipedJSON(
res, id
))
}catch (e: Exception){
return getFromYTDL(url)
getFromYTDL(url)
}
}
@ -303,6 +207,7 @@ class InfoUtil(private val context: Context) {
}
formats.add(formatObj)
}
}
if (obj.has("videoStreams")){
@ -318,6 +223,7 @@ class InfoUtil(private val context: Context) {
}
formats.add(formatObj)
}
}
formats.sortBy { it.filesize }
formats.groupBy { it.format_id }.forEach {
@ -442,42 +348,9 @@ class InfoUtil(private val context: Context) {
}
@Throws(JSONException::class)
fun getTrending(context: Context): ArrayList<ResultItem?> {
fun getTrending(): ArrayList<ResultItem?> {
items = ArrayList()
return if (key!!.isEmpty()) {
getTrendingFromPiped()
} else getTrendingFromKey(context)
}
@Throws(JSONException::class)
fun getTrendingFromKey(context: Context): ArrayList<ResultItem?> {
val url = "https://www.googleapis.com/youtube/v3/videos?part=snippet&chart=mostPopular&videoCategoryId=10&regionCode=$countryCODE&maxResults=25&key=$key"
//short data
val res = genericRequest(url)
//extra data from the same videos
val contentDetails =
genericRequest("https://www.googleapis.com/youtube/v3/videos?part=contentDetails&chart=mostPopular&videoCategoryId=10&regionCode=$countryCODE&maxResults=25&key=$key")
if (!contentDetails.has("items")) return ArrayList()
val dataArray = res.getJSONArray("items")
val extraDataArray = contentDetails.getJSONArray("items")
for (i in 0 until dataArray.length()) {
val element = dataArray.getJSONObject(i)
val snippet = element.getJSONObject("snippet")
var duration = extraDataArray.getJSONObject(i).getJSONObject("contentDetails")
.getString("duration")
duration = formatDuration(duration)
snippet.put("videoID", element.getString("id"))
snippet.put("duration", duration)
fixThumbnail(snippet)
val v = createVideofromJSON(snippet)
if (v == null || v.thumb.isEmpty()) {
continue
}
v.playlistTitle = context.getString(R.string.trendingPlaylist)
items.add(v)
}
return items
return getTrendingFromPiped()
}
private fun getTrendingFromPiped(): ArrayList<ResultItem?> {
@ -521,13 +394,17 @@ class InfoUtil(private val context: Context) {
val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?")
val m = p.matcher(url)
val formatSource = sharedPreferences.getString("formats_source", "yt-dlp")
return if(m.find() && formatSource == "piped"){
val id = getIDFromYoutubeURL(url)
val res = genericRequest(pipedURL + "/streams/" + id)
if (res.length() == 0) getFromYTDL(url)[0]!!
val item = createVideoFromPipedJSON(res, id)
item!!.formats
}else{
return try {
if(m.find() && formatSource == "piped"){
val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) getFromYTDL(url)[0]!!
val item = createVideoFromPipedJSON(res, id)
item!!.formats
}else{
getFormatsFromYTDL(url)
}
}catch (e: Exception){
getFormatsFromYTDL(url)
}
}
@ -683,13 +560,24 @@ class InfoUtil(private val context: Context) {
items = ArrayList()
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
try {
val request = YoutubeDLRequest(query)
val request : YoutubeDLRequest
if (query.contains("http")){
request = YoutubeDLRequest(query)
}else{
request = YoutubeDLRequest(emptyList())
if (searchEngine == "ytsearchmusic"){
request.addOption("--default-search", "https://music.youtube.com/search?q=")
request.addOption("ytsearch25:\"${query}\"")
}else{
request.addOption("${searchEngine}25:\"${query}\"")
}
}
request.addOption("--flat-playlist")
request.addOption("-j")
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
if (!query.contains("http")) request.addOption("--default-search", "${searchEngine}25")
val cookiesFile = File(context.cacheDir, "cookies.txt")
if (cookiesFile.exists()){
@ -1047,7 +935,8 @@ class InfoUtil(private val context: Context) {
request.addCommands(listOf("--replace-in-metadata","uploader",".*.",downloadItem.author.take(25)))
}
request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$",""))
if (downloadItem.customFileNameTemplate.isBlank()) downloadItem.customFileNameTemplate = "%(uploader)s - %(title)s"
if (downloadItem.customFileNameTemplate.isBlank()) downloadItem.customFileNameTemplate = "%(uploader|${downloadItem.author})s - %(title)s"
downloadItem.customFileNameTemplate.replace("%(uploader)s", "%(uploader|${downloadItem.author})s")
if (downloadItem.downloadSections.isNotBlank()){
downloadItem.downloadSections.split(";").forEach {
@ -1142,11 +1031,12 @@ class InfoUtil(private val context: Context) {
request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
}
request.addOption("-P", tempFileDir.absolutePath)
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters")
request.addOption("-P", tempFileDir.absolutePath)
}else{
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
request.addOption("-o", "${downloadItem.customFileNameTemplate}.%(ext)s")
}
}
@ -1194,9 +1084,11 @@ class InfoUtil(private val context: Context) {
}
if (downloadItem.videoPreferences.writeSubs){
val subFormat = sharedPreferences.getString("sub_format", "srt")
request.addOption("--write-subs")
request.addOption("--write-auto-subs")
request.addOption("--sub-format", "str/ass/best")
request.addOption("--sub-format", "${subFormat}/best")
request.addOption("--convert-subtitles", "srt")
if (!downloadItem.videoPreferences.embedSubs) {
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
@ -1207,11 +1099,12 @@ class InfoUtil(private val context: Context) {
request.addOption("--ppa", "ffmpeg:-an")
}
request.addOption("-P", tempFileDir.absolutePath)
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters")
request.addOption("-P", tempFileDir.absolutePath)
}else{
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
request.addOption("-o", "${downloadItem.customFileNameTemplate}.%(ext)s")
}
}

View file

@ -1,14 +1,39 @@
package com.deniscerri.ytdlnis.util
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.text.Spanned
import android.util.TypedValue
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.text.HtmlCompat
import androidx.core.text.parseAsHtml
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import com.deniscerri.ytdlnis.R
import com.google.android.material.color.DynamicColors
object ThemeUtil {
sealed class AppIcon(
@DrawableRes val iconResource: Int,
val activityAlias: String
) {
object Default : AppIcon(R.mipmap.ic_launcher, "Default")
object Light : AppIcon(R.mipmap.ic_launcher_light, "LightIcon")
object Dark : AppIcon(R.mipmap.ic_launcher_dark, "DarkIcon")
}
private val availableIcons = listOf(
AppIcon.Default,
AppIcon.Light,
AppIcon.Dark
)
fun updateTheme(activity: AppCompatActivity) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
@ -31,12 +56,79 @@ object ThemeUtil {
activity.theme.applyStyle(R.style.Pure, true)
}
//disable old icons
for (appIcon in availableIcons) {
val activityClass = "com.deniscerri.ytdlnis." + appIcon.activityAlias
// remove old icons
activity.packageManager.setComponentEnabledSetting(
ComponentName(activity.packageName, activityClass),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
}
when (sharedPreferences.getString("ytdlnis_theme", "System")!!) {
"System" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
"Light" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
"Dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
else -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
"System" -> {
//set dynamic icon
val aliasComponentName = ComponentName(activity, "com.deniscerri.ytdlnis.Default")
activity.packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
"Light" -> {
//set light icon
val aliasComponentName = ComponentName(activity, "com.deniscerri.ytdlnis.LightIcon")
activity.packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
"Dark" -> {
//set dark icon
val aliasComponentName = ComponentName(activity, "com.deniscerri.ytdlnis.DarkIcon")
activity.packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
else -> {
//set dynamic icon
val aliasComponentName = ComponentName(activity, "com.deniscerri.ytdlnis.Default")
activity.packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
}
}
fun getThemeColor(context: Context, colorCode: Int): Int {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val accent = sharedPreferences.getString("theme_accent", "blue")
return if (accent == "Default" || accent == "blue"){
"c22727".toInt(16)
}else{
val value = TypedValue()
context.theme.resolveAttribute(colorCode, value, true)
value.data
}
}
/**
* Get the styled app name
*/
fun getStyledAppName(context: Context): Spanned {
val colorPrimary = getThemeColor(context, androidx.appcompat.R.attr.colorPrimaryDark)
val hexColor = "#%06X".format(0xFFFFFF and colorPrimary)
return "<span style='color:$hexColor';>YTDL</span>nis"
.parseAsHtml(HtmlCompat.FROM_HTML_MODE_COMPACT)
}
}

View file

@ -54,12 +54,20 @@ import java.util.Calendar
object UiUtil {
@SuppressLint("SetTextI18n")
fun populateFormatCard(formatCard : MaterialCardView, chosenFormat: Format, audioFormats: List<Format>?){
formatCard.findViewById<TextView>(R.id.container).text = chosenFormat.container.uppercase()
fun populateFormatCard(context: Context, formatCard : MaterialCardView, chosenFormat: Format, audioFormats: List<Format>?){
var formatNote = chosenFormat.format_note
if (formatNote.isEmpty()) formatNote = context.getString(R.string.defaultValue)
else if (formatNote == "best") formatNote = context.getString(R.string.best_quality)
else if (formatNote == "worst") formatNote = context.getString(R.string.worst_quality)
var container = chosenFormat.container
if (container == "Default") container = context.getString(R.string.defaultValue)
formatCard.findViewById<TextView>(R.id.container).text = container.uppercase()
if (audioFormats.isNullOrEmpty()){
formatCard.findViewById<TextView>(R.id.format_note).text = chosenFormat.format_note.uppercase()
formatCard.findViewById<TextView>(R.id.format_note).text = formatNote.uppercase()
}else{
val title = "${chosenFormat.format_note.uppercase()} + [${audioFormats.joinToString("/") { it.format_note }}]"
val title = "${formatNote.uppercase()} + [${audioFormats.joinToString("/") { it.format_note }}]"
formatCard.findViewById<TextView>(R.id.format_note).text = title
}
formatCard.findViewById<TextView>(R.id.format_id).text = "id: ${chosenFormat.format_id}"

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M6,17h3l2,-4L11,7L5,7v6h3zM14,17h3l2,-4L19,7h-6v6h3z"/>
</vector>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -3,6 +3,7 @@
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="200dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">

View file

@ -62,7 +62,7 @@
android:scrollbars="none"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/action_button"
app:layout_constraintEnd_toStartOf="@+id/download_type"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -71,7 +71,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="title,action_button" />
app:constraint_referenced_ids="title,download_type" />
<HorizontalScrollView
android:layout_width="wrap_content"

View file

@ -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>

View file

@ -21,4 +21,10 @@
android:icon="@drawable/ic_delete_all"
android:title="@string/clear_errored"
app:showAsAction="never" />
<item
android:id="@+id/clear_saved"
android:icon="@drawable/ic_delete_all"
android:title="@string/clear_saved"
app:showAsAction="never" />
</menu>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -28,18 +28,23 @@
android:label="MainSettingsFragment" >
<action
android:id="@+id/action_mainSettingsFragment_to_appearanceSettingsFragment"
app:destination="@id/appearanceSettingsFragment" />
app:destination="@id/appearanceSettingsFragment"
app:enterAnim="@anim/fade_in" />
<action
android:id="@+id/action_mainSettingsFragment_to_folderSettingsFragment"
app:destination="@id/folderSettingsFragment" />
app:destination="@id/folderSettingsFragment"
app:enterAnim="@anim/fade_in" />
<action
android:id="@+id/action_mainSettingsFragment_to_downloadSettingsFragment"
app:destination="@id/downloadSettingsFragment" />
app:destination="@id/downloadSettingsFragment"
app:enterAnim="@anim/fade_in" />
<action
android:id="@+id/action_mainSettingsFragment_to_processingSettingsFragment"
app:destination="@id/processingSettingsFragment" />
app:destination="@id/processingSettingsFragment"
app:enterAnim="@anim/fade_in" />
<action
android:id="@+id/action_mainSettingsFragment_to_updateSettingsFragment"
app:destination="@id/updateSettingsFragment" />
app:destination="@id/updateSettingsFragment"
app:enterAnim="@anim/fade_in" />
</fragment>
</navigation>

View file

@ -271,7 +271,7 @@
<string name="move_temporary_files_summary">Transfero skedarët e përkohshëm në dosjen e shkarkimeve</string>
<string name="format_filtering_hint_2">Të gjithë artikujt duhet të jenë audio ose video për të përdorur këtë opsion</string>
<string name="custom_audio_quality">Përdor cilësinë e personalizuar të audios</string>
<string name="piped_instance">Shembull me tubacione</string>
<string name="piped_instance">Instanca e PIPED</string>
<string name="piped_instance_summary">Shkruani një Server API PIPED, aplikacioni mund të përdorë për kërkimet dhe formatet në youtube</string>
<string name="retries">Riprovimet</string>
<string name="fragment_retries">Riprovimet e fragmentit</string>

View file

@ -161,6 +161,7 @@
<array name="search_engines">
<item>YouTube</item>
<item>YouTube Music</item>
<item>Nico video</item>
<item>PRX Series</item>
<item>PRX Stories</item>
@ -170,10 +171,14 @@
<item>Google Video</item>
<item>Bilibili</item>
<item>Netverse</item>
<item>All Clips of a trovo.live channel</item>
<item>All VODs of a trovo.live channel</item>
<item>web.archive.org saved youtube videos</item>
</array>
<array name="search_engines_values">
<item>ytsearch</item>
<item>ytsearchmusic</item>
<item>nicosearch</item>
<item>PRX Series</item>
<item>PRX Stories</item>
@ -183,6 +188,9 @@
<item>gvsearch</item>
<item>bilisearch</item>
<item>netsearch</item>
<item>trovoclip</item>
<item>trovovod</item>
<item>ytarchive</item>
</array>
<array name="start_destination">
@ -740,4 +748,23 @@
<item>searchHistory</item>
</array>
<string-array name="sub_formats">
<item>srt</item>
<item>ass</item>
<item>lrc</item>
<item>vtt</item>
</string-array>
<string-array name="hide_thumbnail">
<item>@string/home</item>
<item>@string/downloads</item>
<item>@string/download_queue</item>
</string-array>
<string-array name="hide_thumbnail_values">
<item>home</item>
<item>downloads</item>
<item>queue</item>
</string-array>
</resources>

View file

@ -208,7 +208,7 @@
<string name="subtitle_languages">Subtitle languages</string>
<string name="format_source">Formats Source</string>
<string name="video_recommendations">Video Recommendations</string>
<string name="video_recommendations_summary">Get recommended YouTube videos on the home screen from Invidious</string>
<string name="video_recommendations_summary">Get recommended YouTube videos on the home screen from PIPED API</string>
<string name="preferred_search_engine">Preferred Search Engine</string>
<string name="preferred_search_engine_summary">The search engine to use for in-app searches</string>
<string name="format_filtering_hint">All items must be of the same type to use this option</string>
@ -283,8 +283,14 @@
<string name="fragment_retries">Fragment Retries</string>
<string name="format_order">Format Order</string>
<string name="force_keyframes">Force Keyframes At Cuts</string>
<string name="force_keyframes_summary">Accurate Video Cuts. Slower</string>
<string name="force_keyframes_summary">Slower process, but more accurate cuts</string>
<string name="use_extra_commands">Add Extra Commands</string>
<string name="use_extra_commands_summary">For Audio / Video downloads. Add extra commands along with the GUI configuration</string>
<string name="current">Current Command</string>
<string name="subtitle_format">Subtitle Format\n</string>
<string name="hide_thumbnails">Hide Thumbnails</string>
<string name="hide_terminal">Hide Terminal from share menu</string>
<string name="saved">Saved</string>
<string name="clear_saved">Clear Saved</string>
<string name="save_for_later">Save Download for Later?</string>
</resources>

View file

@ -55,4 +55,8 @@
<item name="maxLines">2</item>
<item name="animationMode">slide</item>
</style>
<style name="FullScreenDialogTheme" parent="MaterialAlertDialog.Material3">
<item name="android:windowIsFloating">false</item>
</style>
</resources>

View file

@ -35,6 +35,15 @@
app:summary="@string/download_over_metered_networks_summary"
app:title="@string/download_over_metered_networks" />
<MultiSelectListPreference
app:icon="@drawable/baseline_hide_image_24"
app:dialogTitle="@string/hide_thumbnails"
app:entries="@array/hide_thumbnail"
app:entryValues="@array/hide_thumbnail_values"
app:key="hide_thumbnails"
app:title="@string/hide_thumbnails" />
<EditTextPreference
android:icon="@drawable/baseline_network_locked_24"
app:key="proxy"
@ -52,13 +61,6 @@
app:summary="@string/preferred_download_type_summary"
app:title="@string/preferred_download_type" />
<EditTextPreference
android:icon="@drawable/ic_key"
app:key="api_key"
app:defaultValue=""
app:summary="@string/api_key_summary"
app:title="@string/api_key" />
<SeekBarPreference
app:dependency="aria2"
android:defaultValue="3"

View file

@ -38,6 +38,16 @@
app:summary="@string/pure_theme_summary"
app:title="@string/high_contrast" />
<MultiSelectListPreference
app:icon="@drawable/baseline_hide_image_24"
app:dialogTitle="@string/hide_thumbnails"
app:entries="@array/hide_thumbnail"
app:entryValues="@array/hide_thumbnail_values"
app:key="hide_thumbnails"
app:title="@string/hide_thumbnails" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/misc">
@ -93,5 +103,12 @@
android:key="swipe_gestures"
app:summary="@string/swipe_gestures_summary"
app:title="@string/swipe_gestures" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_share_24"
android:key="hide_terminal"
app:title="@string/hide_terminal" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -68,6 +68,15 @@
app:summary="@string/force_keyframes_summary"
app:title="@string/force_keyframes" />
<ListPreference
android:defaultValue="srt"
android:entries="@array/sub_formats"
android:entryValues="@array/sub_formats"
android:icon="@drawable/baseline_format_quote_24"
app:key="sub_format"
app:useSimpleSummaryProvider="true"
app:title="@string/subtitle_format" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"

View file

@ -49,7 +49,7 @@
<EditTextPreference
app:key="piped_instance"
app:defaultValue="https://pipedapi.syncpundit.io"
app:defaultValue="https://api.piped.projectsegfau.lt"
android:summary="@string/piped_instance_summary"
app:title="@string/piped_instance" />