alot of stuff

- include search history when searching
- removed scroll bug from command tab
- added spacing between command template title and ok button in selection card
- made download progress not dublicate in terminal
- made ability to store terminal state
- added ability to create multiple terminal instances and show them as a list similar to download queue
- fixed thumbnail download not working
- fixed app crashing when clicking on format updated notification
- fixed app crashing when double clicking format on multiple download card
- added custom sponsorblock api preference
- removed contextual app bar when you its enabled and the user taps the log button in the erorred tab
- made app always show quick download card and asynchoronously load data. Quick Download now if its on, it wont load data at all
- added shimmer when loading data in the download card
- fixed app showing no formats if there were no common formats. Now it will give you generic formats
- made open command template list be half the screen, shortcuts third of the screen so the user can see what its being added
- fixed sometimes app slipping queued downloads even though its told to pause all
- fixed trim filenames cutting files too short
- made mediastore scanning of files one by one
- fixed filename template not working in multiple download card
- fixed -F in terminal not being inline
- added preferred audio codec
- made auto update on boot if there are no active downloads
- fixed format text overlapping
- added a new error activity dialog in cases yt-dlp data fetching in the home screen fails. You can copy the log
pull/299/head
deniscerri 3 years ago
parent a31f3f0804
commit 25a09e7604
No known key found for this signature in database
GPG Key ID: 95C43D517D830350

@ -19,7 +19,7 @@ if (versionBuild > 0){
android {
namespace 'com.deniscerri.ytdlnis'
compileSdk 33
compileSdk 34
try {
def propertiesFile = rootProject.file("local.properties")
@ -128,7 +128,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:$appCompatVer"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation 'com.google.android.material:material:1.9.0-rc01'
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.3.1'
implementation 'androidx.preference:preference-ktx:1.2.1'

@ -0,0 +1,544 @@
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "6b5efd73749a7138035e1f00349715c8",
"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)",
"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
}
],
"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, `useAsExtraCommand` INTEGER NOT NULL DEFAULT 0)",
"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": "useAsExtraCommand",
"columnName": "useAsExtraCommand",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"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": []
},
{
"tableName": "terminalDownloads",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `command` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "command",
"columnName": "command",
"affinity": "TEXT",
"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, '6b5efd73749a7138035e1f00349715c8')"
]
}
}

@ -93,7 +93,7 @@
</intent-filter>
</activity>
<activity
android:name=".ui.more.TerminalActivity"
android:name=".ui.more.terminal.TerminalActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:exported="true"
android:label="@string/terminal"
@ -107,7 +107,7 @@
<activity-alias
android:name=".terminalShareAlias"
android:targetActivity=".ui.more.TerminalActivity"
android:targetActivity=".ui.more.terminal.TerminalActivity"
android:enabled="false"
android:icon="@mipmap/terminal_icon"
android:roundIcon="@mipmap/terminal_icon_round"
@ -211,6 +211,16 @@
</intent-filter>
</activity>
<activity
android:name=".ui.ErrorDialogActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleInstance"
android:theme="@style/Theme.BottomSheet">
</activity>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"

@ -11,9 +11,13 @@ import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.deniscerri.ytdlnis.work.UpdateYTDLWorker
import com.google.android.material.snackbar.Snackbar
import com.yausername.aria2c.Aria2c
import com.yausername.ffmpeg.FFmpeg
import com.yausername.youtubedl_android.YoutubeDL
@ -44,34 +48,10 @@ class App : Application() {
initLibraries()
val appVer = sharedPreferences.getString("version", "")!!
if(appVer.isEmpty() || appVer != BuildConfig.VERSION_NAME){
UpdateUtil(this@App).updateYoutubeDL()
sharedPreferences.edit(commit = true){
putString("version", BuildConfig.VERSION_NAME)
}
}
if (sharedPreferences.getBoolean("auto_update_ytdlp", true)){
//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
)
}else{
WorkManager.getInstance(this@App).cancelUniqueWork("ytdlp-Updater")
}
}catch (e: Exception){
Looper.prepare().runCatching {
Toast.makeText(this@App, e.message, Toast.LENGTH_SHORT).show()

@ -34,6 +34,7 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
@ -51,9 +52,12 @@ import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.navigation.NavigationView
import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.BufferedReader
@ -233,6 +237,19 @@ class MainActivity : BaseActivity() {
"Queue" -> navController.navigate(R.id.downloadQueueMainFragment)
}
if (sharedPreferences.getBoolean("auto_update_ytdlp", false)){
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
if(DBManager.getInstance(this@MainActivity).downloadDao.getDownloadsCountByStatus(listOf("Active", "Queued")) == 0){
if (UpdateUtil(this@MainActivity).updateYoutubeDL() == YoutubeDL.UpdateStatus.DONE) {
val version = YoutubeDL.getInstance().version(context)
Snackbar.make(findViewById(android.R.id.content),
this@MainActivity.getString(R.string.ytld_update_success) + " [${version}]",
Snackbar.LENGTH_LONG).show()
}
}
}
}
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
@ -350,8 +367,7 @@ class MainActivity : BaseActivity() {
Toast.makeText(context, "Couldn't read file", Toast.LENGTH_LONG).show()
return
}
val item = downloadViewModel.createEmptyResultItem(f.absolutePath)
val bottomSheet = DownloadBottomSheetDialog(item, DownloadViewModel.Type.command, null, true)
val bottomSheet = DownloadBottomSheetDialog(type = DownloadViewModel.Type.command, result = downloadViewModel.createEmptyResultItem(f.absolutePath))
bottomSheet.show(supportFragmentManager, "downloadSingleSheet")
}else{
val `is` = contentResolver.openInputStream(uri!!)

@ -0,0 +1,89 @@
package com.deniscerri.ytdlnis.adapter
import android.app.Activity
import android.content.SharedPreferences
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.TerminalItem
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.progressindicator.LinearProgressIndicator
class TerminalDownloadsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<TerminalItem?, TerminalDownloadsAdapter.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) : 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_terminal_card, parent, false)
return ViewHolder(cardView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val card = holder.cardView
card.tag = "${item!!.id}##card"
// PROGRESS BAR ----------------------------------------------------
val progressBar = card.findViewById<LinearProgressIndicator>(R.id.progress)
progressBar.tag = "${item.id}##progress"
progressBar.progress = 0
progressBar.isIndeterminate = true
// COMMAND ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.title)
itemTitle.text = item.command.trim()
//OUTPUT
val output = card.findViewById<TextView>(R.id.output)
output.tag = "${item.id}##output"
// STOP BUTTON ----------------------------------
val stopButton = card.findViewById<MaterialButton>(R.id.active_download_stop)
if (stopButton.hasOnClickListeners()) stopButton.setOnClickListener(null)
stopButton.setOnClickListener {onItemClickListener.onCancelClick(item.id)}
card.setOnClickListener {
onItemClickListener.onCardClick(item)
}
}
interface OnItemClickListener {
fun onCancelClick(itemID: Long)
fun onCardClick(item: TerminalItem)
}
companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<TerminalItem> = object : DiffUtil.ItemCallback<TerminalItem>() {
override fun areItemsTheSame(oldItem: TerminalItem, newItem: TerminalItem): Boolean {
val ranged = arrayListOf(oldItem.id, newItem.id)
return ranged[0] == ranged[1]
}
override fun areContentsTheSame(oldItem: TerminalItem, newItem: TerminalItem): Boolean {
return oldItem.command == newItem.command
}
}
}
}

@ -12,8 +12,18 @@ import kotlinx.coroutines.flow.Flow
@TypeConverters(Converters::class)
@Database(
entities = [ResultItem::class, HistoryItem::class, DownloadItem::class, CommandTemplate::class, SearchHistoryItem::class, TemplateShortcut::class, CookieItem::class, LogItem::class],
version = 9,
entities = [
ResultItem::class,
HistoryItem::class,
DownloadItem::class,
CommandTemplate::class,
SearchHistoryItem::class,
TemplateShortcut::class,
CookieItem::class,
LogItem::class,
TerminalItem::class
],
version = 10,
autoMigrations = [
AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3),
@ -22,7 +32,8 @@ import kotlinx.coroutines.flow.Flow
AutoMigration (from = 5, to = 6),
AutoMigration (from = 6, to = 7),
AutoMigration (from = 7, to = 8),
AutoMigration (from = 8, to = 9)
AutoMigration (from = 8, to = 9),
AutoMigration (from = 9, to = 10)
]
)
abstract class DBManager : RoomDatabase(){
@ -33,6 +44,7 @@ abstract class DBManager : RoomDatabase(){
abstract val searchHistoryDao: SearchHistoryDao
abstract val cookieDao: CookieDao
abstract val logDao: LogDao
abstract val terminalDao: TerminalDao
companion object {
//prevents multiple instances of db getting created at the same time

@ -20,7 +20,10 @@ interface DownloadDao {
fun getActiveDownloadsCountFlow() : Flow<Int>
@Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)")
fun getDownloadsCountByStatus(status : List<String>) : Flow<Int>
fun getDownloadsCountByStatusFlow(status : List<String>) : Flow<Int>
@Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)")
fun getDownloadsCountByStatus(status : List<String>) : Int
@Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'")
fun getActiveAndPausedDownloadsList() : List<DownloadItem>

@ -0,0 +1,28 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.DownloadItemSimple
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.TerminalItem
import kotlinx.coroutines.flow.Flow
@Dao
interface TerminalDao {
@Query("SELECT * FROM terminalDownloads ORDER BY id")
fun getActiveTerminalDownloads() : List<TerminalItem>
@Query("SELECT * FROM terminalDownloads ORDER BY id")
fun getActiveTerminalDownloadsFlow() : Flow<List<TerminalItem>>
@Query("SELECT COUNT(*) FROM terminalDownloads")
fun getActiveTerminalsCount() : Int
@Insert
suspend fun insert(item: TerminalItem) : Long
@Query("DELETE from terminalDownloads WHERE id=:id")
suspend fun delete(id: Long)
}

@ -0,0 +1,13 @@
package com.deniscerri.ytdlnis.database.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
@Entity(tableName = "terminalDownloads")
data class TerminalItem(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
var command: String
)

@ -33,36 +33,28 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
suspend fun search(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{
val infoUtil = InfoUtil(context)
try{
if (resetResults) deleteAll()
val res = infoUtil.search(inputQuery)
itemCount.value = res.size
res.forEach {
resultDao.insert(it!!)
}
return res
}catch (ignored: Exception){}
return arrayListOf()
if (resetResults) deleteAll()
val res = infoUtil.search(inputQuery)
itemCount.value = res.size
res.forEach {
resultDao.insert(it!!)
}
return res
}
suspend fun getOne(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{
val infoUtil = InfoUtil(context)
try {
val v = infoUtil.getYoutubeVideo(inputQuery)
if (resetResults) {
deleteAll()
itemCount.value = v.size
}else{
v.forEach { it?.playlistTitle = "ytdlnis-Search" }
}
v.forEach {
resultDao.insert(it!!)
}
return ArrayList(v)
} catch (e: Exception) {
Log.e(tag, e.toString())
val v = infoUtil.getYoutubeVideo(inputQuery)
if (resetResults) {
deleteAll()
itemCount.value = v.size
}else{
v.forEach { it?.playlistTitle = "ytdlnis-Search" }
}
return arrayListOf()
v.forEach {
resultDao.insert(it!!)
}
return ArrayList(v)
}
suspend fun getPlaylist(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{

@ -46,13 +46,9 @@ import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
@ -84,6 +80,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
private var audioContainer: String?
private var videoContainer: String?
private var videoCodec: String?
private var audioCodec: String?
private val dao: DownloadDao
enum class Type {
@ -155,6 +152,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
videoCodec = sharedPreferences.getString("video_codec", "")
audioCodec = sharedPreferences.getString("audio_codec", "")
}
fun deleteDownload(id: Long) = viewModelScope.launch(Dispatchers.IO) {
@ -188,7 +186,9 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
}
fun createDownloadItemFromResult(resultItem: ResultItem, givenType: Type) : DownloadItem {
fun createDownloadItemFromResult(result: ResultItem?, url: String = "", givenType: Type) : DownloadItem {
val resultItem = result ?: createEmptyResultItem(url)
val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
val addChapters = sharedPreferences.getBoolean("add_chapters", false)
@ -386,12 +386,12 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
Type.audio -> {
return cloneFormat (
try {
try{
formats.filter { it.format_note.contains("audio", ignoreCase = true)}
.firstOrNull { audioFormatIDPreference.indexOfFirst { a -> a.contains(it.format_id) } >= 0 || it.container == audioContainer} ?: throw Exception()
}catch (e: Exception){
formats.first { it.format_note.contains("audio", ignoreCase = true) }
}
val theFormats = formats.filter { it.format_note.contains("audio", ignoreCase = true) }
val requirements: MutableList<(Format) -> Boolean> = mutableListOf()
requirements.add {it: Format -> audioFormatIDPreference.contains(it.format_id)}
requirements.add {it: Format -> it.container == audioContainer }
requirements.add {it: Format -> "^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)}
theFormats.maxByOrNull { f -> requirements.count{req -> req(f)} } ?: throw Exception()
}catch (e: Exception){
bestAudioFormat
}
@ -408,7 +408,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
else /*best*/ -> {
val requirements: MutableList<(Format) -> Boolean> = mutableListOf()
requirements.add { it: Format -> formatIDPreference.sorted().contains(it.format_id) }
requirements.add { it: Format -> formatIDPreference.contains(it.format_id) }
requirements.add { it: Format -> if (videoContainer == "mp4") it.container.equals("mpeg_4", true) else it.container.equals(videoContainer, true)}
requirements.add { it: Format -> it.format_note.contains(videoQualityPreference.split("_")[0].dropLast(1)) }
requirements.add { it: Format -> "^(${videoCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)}
@ -451,7 +451,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
var preferredType = sharedPreferences.getString("preferred_download_type", "video")
items.forEach {
preferredType = getDownloadType(Type.valueOf(preferredType!!), it!!.url).toString()
list.add(createDownloadItemFromResult(it, Type.valueOf(preferredType!!)))
list.add(createDownloadItemFromResult(result = it, givenType = Type.valueOf(preferredType!!)))
}
return list
}
@ -656,7 +656,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
fun getTotalSize(status: List<DownloadRepository.Status>) : LiveData<Int> {
return dbManager.downloadDao.getDownloadsCountByStatus(status.map { it.toString() }).asLiveData()
return dbManager.downloadDao.getDownloadsCountByStatusFlow(status.map { it.toString() }).asLiveData()
}
fun checkAllQueuedItemsAreScheduledAfterNow(items: List<Long>, inverted: Boolean, currentStartTime: Long) : Boolean {

@ -64,18 +64,20 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
deleteAll()
}
}
suspend fun parseQueries(inputQueries: List<String>){
suspend fun parseQueries(inputQueries: List<String>) : MutableList<ResultItem?>{
state = ResultsState.PROCESSING
if (inputQueries.size == 1){
return if (inputQueries.size == 1){
parseQuery(inputQueries[0], true)
}else {
repository.itemCount.value = inputQueries.size
loadingItems.postValue(true)
val items = mutableListOf<ResultItem?>()
inputQueries.forEach {
parseQuery(it, false)
items.addAll(parseQuery(it, false))
}
state = ResultsState.IDLE
loadingItems.postValue(false)
items
}
}

@ -0,0 +1,110 @@
package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.content.res.Resources
import android.net.ConnectivityManager
import android.os.Looper
import android.util.DisplayMetrics
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.await
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.dao.TerminalDao
import com.deniscerri.ytdlnis.database.models.AudioPreferences
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.DownloadItemSimple
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.models.TerminalItem
import com.deniscerri.ytdlnis.database.models.VideoPreferences
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler
import com.deniscerri.ytdlnis.work.DownloadWorker
import com.deniscerri.ytdlnis.work.TerminalDownloadWorker
import com.google.gson.Gson
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File
import java.util.Locale
import java.util.concurrent.TimeUnit
class TerminalViewModel(private val application: Application) : AndroidViewModel(application) {
private val dbManager: DBManager = DBManager.getInstance(application)
private val dao: TerminalDao = dbManager.terminalDao
private val notificationUtil = NotificationUtil(application)
fun getCount() : Int{
return dao.getActiveTerminalsCount()
}
fun getTerminals() : Flow<List<TerminalItem>> {
return dao.getActiveTerminalDownloadsFlow()
}
suspend fun insert(item: TerminalItem) : Long {
return dao.insert(item)
}
suspend fun delete(id: Long) = CoroutineScope(Dispatchers.IO).launch{
dao.delete(id)
}
fun startTerminalDownloadWorker(item: TerminalItem) = CoroutineScope(Dispatchers.IO).launch {
val workRequest = OneTimeWorkRequestBuilder<TerminalDownloadWorker>()
.setInputData(
Data.Builder()
.putInt("id", item.id.toInt())
.putString("command", item.command)
.build()
)
.addTag("terminal")
.addTag(item.id.toString())
.build()
WorkManager.getInstance(application).beginUniqueWork(
item.id.toString(),
ExistingWorkPolicy.KEEP,
workRequest
).enqueue()
}
fun cancelTerminalDownload(id: Long) = CoroutineScope(Dispatchers.IO).launch{
YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(application).cancelUniqueWork(id.toString())
Thread.sleep(200)
notificationUtil.cancelDownloadNotification(id.toInt())
delete(id)
}
}

@ -108,17 +108,6 @@ class ShareActivity : BaseActivity() {
runCatching { supportFragmentManager.popBackStack() }
quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false) || sharedPreferences.getString("preferred_download_type", "video") == "command")
val loadingBottomSheet = BottomSheetDialog(this)
loadingBottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
loadingBottomSheet.setContentView(R.layout.please_wait_bottom_sheet)
loadingBottomSheet.show()
val cancel = loadingBottomSheet.findViewById<TextView>(R.id.cancel)
cancel!!.setOnClickListener {
resultViewModel.deleteAll()
this.finish()
}
val intentData = when(action){
Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)!!
else -> intent.dataString!!
@ -132,15 +121,16 @@ class ShareActivity : BaseActivity() {
lifecycleScope.launch {
try {
val result = resultViewModel.getItemByURL(inputQuery)
loadingBottomSheet.dismiss()
val result = withContext(Dispatchers.IO){
resultViewModel.getItemByURL(inputQuery)
}
if (result == null) throw Exception()
showDownloadSheet(result)
}catch (e: Exception){
resultViewModel.deleteAll()
if (quickDownload && matcher.matches()){
val result = downloadViewModel.createEmptyResultItem(inputQuery)
loadingBottomSheet.dismiss()
showDownloadSheet(result)
showDownloadSheet(downloadViewModel.createEmptyResultItem(inputQuery))
if (matcher.matches()){
}else{
val res = withContext(Dispatchers.IO){
resultViewModel.parseQuery(inputQuery, true)
@ -149,7 +139,6 @@ class ShareActivity : BaseActivity() {
Toast.makeText(this@ShareActivity, "No Results Found!", Toast.LENGTH_SHORT).show()
exit()
}else{
loadingBottomSheet.dismiss()
if (res.size == 1){
showDownloadSheet(res[0]!!)
}else{
@ -163,12 +152,17 @@ class ShareActivity : BaseActivity() {
}
private fun showDownloadSheet(it: ResultItem){
val downloadType = downloadViewModel.getDownloadType(DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!), it.url)
if (sharedPreferences.getBoolean("download_card", true)){
val bottomSheet = DownloadBottomSheetDialog(it,downloadViewModel.getDownloadType(DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!), it.url), null, quickDownload)
val bottomSheet = DownloadBottomSheetDialog(
result = it,
type = downloadType)
bottomSheet.show(supportFragmentManager, "downloadSingleSheet")
}else{
lifecycleScope.launch(Dispatchers.IO){
val downloadItem = downloadViewModel.createDownloadItemFromResult(it, DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!))
val downloadItem = downloadViewModel.createDownloadItemFromResult(
result = it,
givenType = downloadType)
downloadViewModel.queueDownloads(listOf(downloadItem))
}
this.finish()
@ -184,7 +178,9 @@ class ShareActivity : BaseActivity() {
val downloadItems = mutableListOf<DownloadItem>()
lifecycleScope.launch(Dispatchers.IO){
it.forEach { res ->
val i = downloadViewModel.createDownloadItemFromResult(res!!, DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!))
val i = downloadViewModel.createDownloadItemFromResult(
result = res!!,
givenType = DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!))
i.format = downloadViewModel.getLatestCommandTemplateAsFormat()
downloadItems.add(i)
}

@ -0,0 +1,57 @@
package com.deniscerri.ytdlnis.ui
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.ThemeUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
open class ErrorDialogActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val title = intent.extras?.getString("title") ?: ""
val message = intent.extras?.getString("message") ?: ""
ThemeUtil.updateTheme(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v, insets ->
v.setPadding(0, 0, 0, 0)
insets
}
window.run {
setBackgroundDrawable(ColorDrawable(0))
setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
} else {
setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT)
}
}
val errDialog = MaterialAlertDialogBuilder(this)
.setTitle(title)
.setMessage(message)
.setIcon(R.drawable.ic_cancel)
.setPositiveButton(getString(R.string.copy_log)) { _: DialogInterface?, _: Int ->
val clipboard: ClipboardManager =
getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setText(message)
this.finish()
}.setOnDismissListener {
this.finish()
}
errDialog.show()
super.onCreate(savedInstanceState)
}
}

@ -6,8 +6,6 @@ import android.content.*
import android.content.Context.CLIPBOARD_SERVICE
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@ -23,8 +21,6 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.children
import androidx.core.view.forEach
import androidx.core.view.forEachIndexed
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
@ -45,7 +41,6 @@ import com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.devbrackets.android.exomedia.core.video.mp.NativeVideoDelegate
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
@ -61,7 +56,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.util.*
@ -168,7 +162,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}else if (resultViewModel.repository.itemCount.value == 1){
if (sharedPreferences!!.getBoolean("download_card", true)){
if(it.size == 1 && quickLaunchSheet && parentFragmentManager.findFragmentByTag("downloadSingleSheet") == null){
showSingleDownloadSheet(it[0], DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), false)
showSingleDownloadSheet(
it[0],
DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!)
)
}
}
}else{
@ -212,7 +209,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
if (arguments?.getString("url") != null){
val url = requireArguments().getString("url")
if (inputQueries == null) inputQueries = mutableListOf()
searchBar?.text = url
searchBar?.setText(url)
val argList = url!!.split("\n").toMutableList()
argList.removeAll(listOf("", null))
inputQueries!!.addAll(argList)
@ -242,18 +239,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}
}
if (arguments?.getBoolean("showDownloadsWithUpdatedFormats") == true){
CoroutineScope(Dispatchers.IO).launch {
val ids = arguments?.getLongArray("downloadIds")
val items = mutableListOf<DownloadItem>()
ids?.forEach {
items.add(downloadViewModel.getItemByID(it))
}
val bottomSheet = DownloadMultipleBottomSheetDialog(items.toMutableList())
bottomSheet.show(parentFragmentManager, "downloadMultipleSheet")
}
}
}
override fun onResume() {
@ -267,7 +252,20 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}else{
arguments?.remove("url")
}
arguments?.remove("showDownloadsWithUpdatedFormats")
if (arguments?.getBoolean("showDownloadsWithUpdatedFormats") == true){
arguments?.remove("showDownloadsWithUpdatedFormats")
CoroutineScope(Dispatchers.IO).launch {
val ids = arguments?.getLongArray("downloadIds")
val items = mutableListOf<DownloadItem>()
ids?.forEach {
items.add(downloadViewModel.getItemByID(it))
}
val bottomSheet = DownloadMultipleBottomSheetDialog(items.toMutableList())
bottomSheet.show(parentFragmentManager, "downloadMultipleSheet")
}
}
if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){
lifecycleScope.launch {
updateSearchViewItems(searchView?.editText?.text, linkYouCopied)
@ -412,7 +410,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
R.id.delete_results -> {
resultViewModel.getTrending()
selectedObjects = ArrayList()
searchBar!!.text = ""
searchBar!!.setText("")
downloadAllFabCoordinator!!.visibility = GONE
downloadFabs!!.visibility = GONE
}
@ -538,7 +536,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
if (queryList.isEmpty()) return
if (queryList.size == 1){
searchBar!!.text = searchView.text
searchBar!!.setText(searchView.text)
}
searchView.hide()
@ -552,13 +550,18 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
Thread.sleep(300)
if(sharedPreferences!!.getBoolean("quick_download", false) || sharedPreferences!!.getString("preferred_download_type", "video") == "command"){
if (queryList.size == 1 && queryList.first().matches("(https?://(?:www\\.|(?!www))[a-zA-Z\\d][a-zA-Z\\d-]+[a-zA-Z\\d]\\.\\S{2,}|www\\.[a-zA-Z\\d][a-zA-Z\\d-]+[a-zA-Z\\d]\\.\\S{2,}|https?://(?:www\\.|(?!www))[a-zA-Z\\d]+\\.\\S{2,}|www\\.[a-zA-Z\\d]+\\.\\S{2,})".toRegex())){
val resultItem = downloadViewModel.createEmptyResultItem(queryList.first())
if (sharedPreferences!!.getBoolean("download_card", true)) {
showSingleDownloadSheet(resultItem, DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), true)
showSingleDownloadSheet(
resultItem = downloadViewModel.createEmptyResultItem(queryList.first()),
type = DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!)
)
} else {
lifecycleScope.launch{
val downloadItem = withContext(Dispatchers.IO){
downloadViewModel.createDownloadItemFromResult(resultItem, DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!))
downloadViewModel.createDownloadItemFromResult(
result = downloadViewModel.createEmptyResultItem(queryList.first()),
givenType = DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!)
)
}
downloadViewModel.queueDownloads(listOf(downloadItem))
}
@ -585,11 +588,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
Log.e(TAG, resultsList!![0].toString() + " " + videoURL)
recyclerView!!.findViewWithTag<MaterialButton>("""${item?.url}##$type""")
if (sharedPreferences!!.getBoolean("download_card", true)) {
showSingleDownloadSheet(item!!, type!!, false)
showSingleDownloadSheet(item!!, type!!)
} else {
lifecycleScope.launch{
val downloadItem = withContext(Dispatchers.IO){
downloadViewModel.createDownloadItemFromResult(item!!, type!!)
downloadViewModel.createDownloadItemFromResult(
result = item!!,
givenType = type!!)
}
downloadViewModel.queueDownloads(listOf(downloadItem))
}
@ -599,11 +604,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
override fun onLongButtonClick(videoURL: String, type: DownloadViewModel.Type?) {
Log.e(TAG, type.toString() + " " + videoURL)
val item = resultsList!!.find { it?.url == videoURL }
showSingleDownloadSheet(item!!, type!!, false)
showSingleDownloadSheet(item!!, type!!)
}
private fun showSingleDownloadSheet(resultItem : ResultItem, type: DownloadViewModel.Type, quickDownload: Boolean){
val bottomSheet = DownloadBottomSheetDialog(resultItem, downloadViewModel.getDownloadType(type, resultItem.url), null, quickDownload)
private fun showSingleDownloadSheet(
resultItem: ResultItem,
type: DownloadViewModel.Type
){
val bottomSheet = DownloadBottomSheetDialog(resultItem, downloadViewModel.getDownloadType(type, resultItem.url))
bottomSheet.show(parentFragmentManager, "downloadSingleSheet")
}
@ -696,7 +704,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
R.id.download -> {
lifecycleScope.launch {
if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects!!.size == 1) {
showSingleDownloadSheet(selectedObjects!![0], DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), false)
showSingleDownloadSheet(
selectedObjects!![0],
DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!)
)
}else{
val downloadList = withContext(Dispatchers.IO){
downloadViewModel.turnResultItemsToDownloadItems(selectedObjects!!)
@ -704,7 +715,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
if (sharedPreferences!!.getBoolean("download_card", true)) {
if (selectedObjects!!.size == 1){
showSingleDownloadSheet(selectedObjects!![0], DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), false)
showSingleDownloadSheet(
selectedObjects!![0],
DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!)
)
}else{
val bottomSheet = DownloadMultipleBottomSheetDialog(downloadList.toMutableList())
bottomSheet.show(parentFragmentManager, "downloadMultipleSheet")

@ -77,6 +77,7 @@ class AddExtraCommandsDialog(private val item: DownloadItem?, private val callba
}
@SuppressLint("SetTextI18n")
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -143,9 +144,14 @@ class AddExtraCommandsDialog(private val item: DownloadItem?, private val callba
shortcuts.setOnClickListener {
lifecycleScope.launch {
if (shortcutCount > 0){
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel) {sh ->
text.text.insert(text.selectionStart, " $sh ")
}
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel,
itemSelected = {sh ->
text.setText("${text.text} $sh")
},
itemRemoved = {removed ->
text.setText(text.text.replace("(${Regex.escape(removed)})(?!.*\\1)".toRegex(), ""))
text.setSelection(text.text.length)
})
}
}
}

@ -5,7 +5,6 @@ import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -24,19 +23,16 @@ import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.util.FileUtil
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.tabs.TabLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, private val downloadItem: DownloadItem, private val listener: OnDownloadItemUpdateListener) : BottomSheetDialogFragment() {
//TODO MAKE CONFIGURE LIKE NORMAL SHEET
class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private val currentDownloadItem: DownloadItem, private val listener: OnDownloadItemUpdateListener) : BottomSheetDialogFragment() {
private lateinit var tabLayout: TabLayout
private lateinit var viewPager2: ViewPager2
private lateinit var fragmentAdapter : DownloadFragmentAdapter
@ -80,17 +76,12 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri
overScrollMode = View.OVER_SCROLL_NEVER
}
val fragments = mutableListOf<Fragment>(
DownloadAudioFragment(resultItem, downloadItem),
DownloadVideoFragment(resultItem, downloadItem)
)
var commandTemplateNr = 0
lifecycleScope.launch{
withContext(Dispatchers.IO){
commandTemplateNr = commandTemplateDao.getTotalNumber()
if(commandTemplateNr > 0){
fragments.add(DownloadCommandFragment(resultItem, downloadItem))
}else{
if(commandTemplateNr <= 0){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.alpha = 0.3f
}
@ -98,7 +89,7 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri
}
//check if the item has formats and its audio-only
val isAudioOnly = resultItem.formats.isNotEmpty() && resultItem.formats.none { !it.format_note.contains("audio") }
val isAudioOnly = result.formats.isNotEmpty() && result.formats.none { !it.format_note.contains("audio") }
if (isAudioOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
@ -107,15 +98,15 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri
val fragmentManager = parentFragmentManager
fragmentAdapter = DownloadFragmentAdapter(
resultItem,
fragmentManager,
lifecycle,
fragments
result,
currentDownloadItem
)
viewPager2.adapter = fragmentAdapter
viewPager2.isSaveFromParentEnabled = false
when(downloadItem.type) {
when(currentDownloadItem.type) {
Type.audio -> {
tabLayout.selectTab(tabLayout.getTabAt(0))
viewPager2.setCurrentItem(0, false)
@ -172,17 +163,17 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri
val ok = view.findViewById<Button>(R.id.bottom_sheet_ok)
ok!!.setOnClickListener {
val item = getDownloadItem()
onDownloadItemUpdateListener.onDownloadItemUpdate(resultItem.id, item)
onDownloadItemUpdateListener.onDownloadItemUpdate(result.id, item)
dismiss()
}
val link = view.findViewById<Button>(R.id.bottom_sheet_link)
link.text = resultItem.url
link.text = result.url
link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), resultItem.url, null)
UiUtil.openLinkIntent(requireContext(), result.url, null)
}
link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), resultItem.url, null)
UiUtil.copyLinkToClipBoard(requireContext(), result.url, null)
true
}
@ -211,10 +202,10 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri
if (viewPager2.currentItem == 1) 0 else 1
)
resultItem.title = prevDownloadItem.title
resultItem.author = prevDownloadItem.author
downloadItem.title = prevDownloadItem.title
downloadItem.author = prevDownloadItem.author
result.title = prevDownloadItem.title
result.author = prevDownloadItem.author
currentDownloadItem.title = prevDownloadItem.title
currentDownloadItem.author = prevDownloadItem.author
when(viewPager2.currentItem){
0 -> {

@ -12,7 +12,8 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isEmpty
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -24,7 +25,6 @@ import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
@ -37,7 +37,7 @@ import kotlinx.coroutines.withContext
import java.io.File
class DownloadAudioFragment(private var resultItem: ResultItem, private var currentDownloadItem: DownloadItem?) : Fragment(), TitleAuthorSync {
class DownloadAudioFragment(private var resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "") : Fragment(), GUISync {
private var fragmentView: View? = null
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
@ -69,18 +69,20 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO) {
if (currentDownloadItem != null){
//object cloning
val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java)
}else{
downloadViewModel.createDownloadItemFromResult(resultItem, Type.audio)
downloadViewModel.createDownloadItemFromResult(resultItem, url, Type.audio)
}
}
val sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(requireContext())
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
try {
title = view.findViewById(R.id.title_textinput)
title.editText!!.setText(downloadItem.title)
if (title.editText?.text?.isEmpty() == true){
title.editText!!.setText(downloadItem.title)
}
title.editText!!.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
@ -88,9 +90,17 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
downloadItem.title = p0.toString()
}
})
title.setEndIconOnClickListener {
if (resultItem != null){
title.editText?.setText(resultItem?.title)
}
title.endIconDrawable = null
}
author = view.findViewById(R.id.author_textinput)
author.editText!!.setText(downloadItem.author)
if (author.editText?.text?.isEmpty() == true){
author.editText!!.setText(downloadItem.author)
}
author.editText!!.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
@ -98,6 +108,26 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
downloadItem.author = p0.toString()
}
})
author.setEndIconOnClickListener {
if (resultItem != null){
author.editText?.setText(resultItem?.author)
}
author.endIconDrawable = null
}
if (savedInstanceState?.containsKey("updated") == true){
if (!listOf(resultItem?.title, downloadItem.title).contains(title.editText?.text.toString())){
title.endIconDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_refresh)
downloadItem.title = title.editText?.text.toString()
}
if (!listOf(resultItem?.author, downloadItem.author).contains(author.editText?.text.toString())){
author.endIconDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_refresh)
downloadItem.author = author.editText?.text.toString()
}
}
saveDir = view.findViewById(R.id.outputPath)
saveDir.editText!!.setText(
FileUtil.formatPath(downloadItem.downloadPath)
@ -109,7 +139,18 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
audioPathResultLauncher.launch(intent)
val callback = FileUtil.getURIFromResult(this@DownloadAudioFragment, requireActivity()){result ->
downloadItem.downloadPath = result
//downloadViewModel.updateDownload(downloadItem)
saveDir.editText?.setText(FileUtil.formatPath(result), TextView.BufferType.EDITABLE)
val free = FileUtil.convertFileSize(
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace)
freeSpace.text = String.format( getString(R.string.freespace) + ": " + free)
if (free == "?") freeSpace.visibility = View.GONE
}
callback.launch(intent)
}
freeSpace = view.findViewById(R.id.freespace)
val free = FileUtil.convertFileSize(
@ -119,7 +160,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
var formats = mutableListOf<Format>()
if (currentDownloadItem == null) {
formats.addAll(resultItem.formats.filter { it.format_note.contains("audio", ignoreCase = true) })
formats.addAll(resultItem?.formats?.filter { it.format_note.contains("audio", ignoreCase = true) } ?: listOf())
}else{
//if its updating a present downloaditem and its the wrong category
if (currentDownloadItem!!.type != Type.audio){
@ -153,9 +194,11 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, null)
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultItem.formats.removeAll(formats.toSet())
resultItem.formats.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) })
resultViewModel.update(resultItem)
resultItem?.formats?.removeAll(formats.toSet())
resultItem?.formats?.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) })
if (resultItem != null){
resultViewModel.update(resultItem!!)
}
}
}
formats = allFormats.first().filter { !genericAudioFormats.contains(it) }.toMutableList()
@ -209,7 +252,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
},
sponsorBlockItemsSet = { values, checkedItems ->
downloadItem.audioPreferences.sponsorBlockFilters.clear()
for (i in 0 until checkedItems.size) {
for (i in checkedItems.indices) {
if (checkedItems[i]) {
downloadItem.audioPreferences.sponsorBlockFilters.add(values[i])
}
@ -217,7 +260,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
},
cutClicked = {cutVideoListener ->
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem.urls, resultItem.chapters, cutVideoListener)
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem?.urls ?: "", resultItem?.chapters ?: listOf(), cutVideoListener)
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
}
},
@ -242,29 +285,15 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
downloadItem.title = t
downloadItem.author = a
title.editText?.setText(t)
title.endIconDrawable = null
author.editText?.setText(a)
title.endIconDrawable = null
}
private var audioPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
downloadItem.downloadPath = result.data?.data.toString()
//downloadViewModel.updateDownload(downloadItem)
saveDir.editText?.setText(FileUtil.formatPath(result.data?.data.toString()), TextView.BufferType.EDITABLE)
val free = FileUtil.convertFileSize(
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace)
freeSpace.text = String.format( getString(R.string.freespace) + ": " + free)
if (free == "?") freeSpace.visibility = View.GONE
}
override fun updateUI(res: ResultItem?){
resultItem = res
val state = Bundle()
state.putBoolean("updated", true)
onViewCreated(requireView(),savedInstanceState = state)
}
}

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Bundle
import android.text.format.DateFormat
@ -15,17 +16,18 @@ import android.view.ViewGroup
import android.widget.Button
import android.widget.LinearLayout
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.deniscerri.ytdlnis.R
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.Format
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
@ -34,17 +36,19 @@ import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.receiver.ShareActivity
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.*
import java.text.SimpleDateFormat
import java.util.*
class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val type: Type, private val downloadItem: DownloadItem?,private val quickDownload: Boolean) : BottomSheetDialogFragment() {
class DownloadBottomSheetDialog(private var result: ResultItem, private val type: Type, private var currentDownloadItem: DownloadItem? = null) : BottomSheetDialogFragment() {
private lateinit var tabLayout: TabLayout
private lateinit var viewPager2: ViewPager2
private lateinit var fragmentAdapter : DownloadFragmentAdapter
@ -53,22 +57,22 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var commandTemplateDao : CommandTemplateDao
private lateinit var infoUtil: InfoUtil
private lateinit var sharedPreferences : SharedPreferences
private lateinit var view: View
private lateinit var downloadAudioFragment: DownloadAudioFragment
private lateinit var downloadVideoFragment: DownloadVideoFragment
private lateinit var downloadCommandFragment: DownloadCommandFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao
infoUtil = InfoUtil(requireContext())
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
}
@SuppressLint("RestrictedApi")
@SuppressLint("RestrictedApi", "InflateParams")
override fun setupDialog(dialog: Dialog, style: Int) {
super.setupDialog(dialog, style)
val view = LayoutInflater.from(context).inflate(R.layout.download_bottom_sheet, null)
view = LayoutInflater.from(context).inflate(R.layout.download_bottom_sheet, null)
dialog.setContentView(view)
dialog.setOnShowListener {
@ -89,18 +93,12 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
overScrollMode = View.OVER_SCROLL_NEVER
}
downloadAudioFragment = DownloadAudioFragment(resultItem, downloadItem)
downloadVideoFragment = DownloadVideoFragment(resultItem, downloadItem)
val fragments = mutableListOf<Fragment>(downloadAudioFragment, downloadVideoFragment)
var commandTemplateNr = 0
lifecycleScope.launch{
withContext(Dispatchers.IO){
commandTemplateNr = commandTemplateDao.getTotalNumber()
if (!Patterns.WEB_URL.matcher(resultItem.url).matches()) commandTemplateNr++
if(commandTemplateNr > 0){
downloadCommandFragment = DownloadCommandFragment(resultItem, downloadItem)
fragments.add(downloadCommandFragment)
}else{
if (!Patterns.WEB_URL.matcher(result.url).matches()) commandTemplateNr++
if(commandTemplateNr <= 0){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.alpha = 0.3f
}
@ -108,14 +106,15 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
}
//check if the item has formats and its audio-only
val isAudioOnly = resultItem.formats.isNotEmpty() && resultItem.formats.none { !it.format_note.contains("audio") }
val formats = result.formats
val isAudioOnly = formats.isNotEmpty() && formats.none { !it.format_note.contains("audio") }
if (isAudioOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
}
//check if the item is coming from a text file
val isCommandOnly = (type == Type.command && !Patterns.WEB_URL.matcher(resultItem.url).matches())
val isCommandOnly = (type == Type.command && !Patterns.WEB_URL.matcher(result.url).matches())
if (isCommandOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(0)?.isClickable = false
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(0)?.alpha = 0.3f
@ -127,10 +126,10 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
val fragmentManager = parentFragmentManager
fragmentAdapter = DownloadFragmentAdapter(
resultItem,
fragmentManager,
lifecycle,
fragments
result,
currentDownloadItem
)
viewPager2.adapter = fragmentAdapter
@ -238,36 +237,23 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
val link = view.findViewById<Button>(R.id.bottom_sheet_link)
val updateItem = view.findViewById<Button>(R.id.update_item)
if (Patterns.WEB_URL.matcher(resultItem.url).matches()){
link.text = resultItem.url
if (Patterns.WEB_URL.matcher(result.url).matches()){
link.text = result.url
link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), resultItem.url, null)
UiUtil.openLinkIntent(requireContext(), result.url, null)
}
link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), resultItem.url, null)
UiUtil.copyLinkToClipBoard(requireContext(), result.url, null)
true
}
if (quickDownload) {
(updateItem.parent as LinearLayout).visibility = View.VISIBLE
updateItem.setOnClickListener {
if (activity is ShareActivity) {
dismiss()
val intent = Intent(context, ShareActivity::class.java)
intent.action = Intent.ACTION_SEND
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, resultItem.url)
intent.putExtra("quick_download", false)
startActivity(intent)
}else{
dismiss()
val bundle = Bundle()
bundle.putString("url", resultItem.url)
findNavController().popBackStack(R.id.homeFragment, true)
findNavController().navigate(
R.id.homeFragment,
bundle
)
//if auto-update after the card is open is off
if (result.title.isEmpty() && currentDownloadItem == null) {
if(sharedPreferences.getBoolean("quick_download", false)) {
(updateItem.parent as LinearLayout).visibility = View.VISIBLE
updateItem.setOnClickListener {
(updateItem.parent as LinearLayout).visibility = View.GONE
initUpdateData(view)
}
}
}else{
@ -278,6 +264,12 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
link.visibility = View.GONE
updateItem.visibility = View.GONE
}
//update in the background if there is no data
if(result.title.isEmpty() && currentDownloadItem == null && !sharedPreferences.getBoolean("quick_download", false)){
initUpdateData(view)
}
}
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{
return when(selectedTabPosition){
@ -300,11 +292,7 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
val prevDownloadItem = getDownloadItem(
if (viewPager2.currentItem == 1) 0 else 1
)
resultItem.title = prevDownloadItem.title
resultItem.author = prevDownloadItem.author
downloadItem?.title = prevDownloadItem.title
downloadItem?.author = prevDownloadItem.author
fragmentAdapter.setTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
when(viewPager2.currentItem){
0 -> {
@ -319,6 +307,75 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
}
}
private fun initUpdateData(v: View){
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO){
val shimmerLoading = v.findViewById<ShimmerFrameLayout>(R.id.shimmer_loading_title)
val title = v.findViewById<View>(R.id.bottom_sheet_title)
val shimmerLoadingSubtitle = v.findViewById<ShimmerFrameLayout>(R.id.shimmer_loading_subtitle)
val subtitle = v.findViewById<View>(R.id.bottom_sheet_subtitle)
withContext(Dispatchers.Main){
title.visibility = View.GONE
subtitle.visibility = View.GONE
shimmerLoading.visibility = View.VISIBLE
shimmerLoadingSubtitle.visibility = View.VISIBLE
shimmerLoading.startShimmer()
shimmerLoadingSubtitle.startShimmer()
}
if (result.url.isBlank()){
withContext(Dispatchers.Main){dismiss()}
}
val result = resultViewModel.parseQueries(listOf(result.url))
if (result.isEmpty()){
withContext(Dispatchers.Main){
dismiss()
}
}
if (result.size == 1 && result[0] != null){
fragmentAdapter.setResultItem(result[0]!!)
withContext(Dispatchers.Main){
runCatching {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateUI(result[0])
}
runCatching {
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f1.updateUI(result[0])
}
}
withContext(Dispatchers.Main){
title.visibility = View.VISIBLE
subtitle.visibility = View.VISIBLE
shimmerLoading.visibility = View.GONE
shimmerLoadingSubtitle.visibility = View.GONE
shimmerLoading.stopShimmer()
shimmerLoadingSubtitle.stopShimmer()
}
}else{
//open multi download card instead
if (activity is ShareActivity){
val preferredType = DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!)
withContext(Dispatchers.Main){
val playlistSelect = SelectPlaylistItemsDialog(items = result, type = preferredType)
parentFragmentManager.addFragmentOnAttachListener { fragmentManager, fragment ->
dismiss()
}
playlistSelect.show(parentFragmentManager, "downloadMultipleSheet")
}
}else{
dismiss()
}
}
}
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
cleanUp()
@ -332,13 +389,11 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
private fun cleanUp(){
kotlin.runCatching {
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("downloadSingleSheet")!!).commit()
parentFragmentManager.beginTransaction().remove(downloadVideoFragment).commit()
parentFragmentManager.beginTransaction().remove(downloadVideoFragment).commit()
if (this::downloadCommandFragment.isInitialized){
parentFragmentManager.beginTransaction().remove(downloadCommandFragment).commit()
repeat(parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.backStackEntryCount ?: 0){
parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.popBackStack()
}
if (activity is ShareActivity){
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("downloadSingleSheet")!!).commit()
if (activity is ShareActivity && !parentFragmentManager.fragments.map { it.tag }.contains("downloadMultipleSheet")){
(activity as ShareActivity).finish()
}
}

@ -41,7 +41,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class DownloadCommandFragment(private val resultItem: ResultItem, private var currentDownloadItem: DownloadItem?) : Fragment() {
class DownloadCommandFragment(private val resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "") : Fragment() {
private var fragmentView: View? = null
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
@ -73,7 +73,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java)
}else{
downloadViewModel.createDownloadItemFromResult(resultItem, DownloadViewModel.Type.command)
downloadViewModel.createDownloadItemFromResult(resultItem, url, DownloadViewModel.Type.command)
}
}
@ -122,7 +122,6 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
chosenCommandView.editText!!.setText("")
}
}
chosenCommandView.editText!!.enableScrollText()
val commandTemplateCard = view.findViewById<MaterialCardView>(R.id.command_card)
runCatching {
@ -230,10 +229,19 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
}
},
shortcutClicked = {
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel) { sh ->
chosenCommandView.editText!!.setText("${chosenCommandView.editText!!.text} $sh")
downloadItem.format.format_note += " $sh"
}
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel,
itemSelected = {
chosenCommandView.editText!!.setText("${chosenCommandView.editText!!.text} $it")
downloadItem.format.format_note = chosenCommandView.editText!!.text.toString()
},
itemRemoved = {removed ->
chosenCommandView.editText!!.setText(chosenCommandView.editText!!.text.replace("(${
Regex.escape(
removed
)
})(?!.*\\1)".toRegex(), "").trimEnd())
downloadItem.format.format_note = chosenCommandView.editText!!.text.toString().trimEnd()
})
}
)
@ -264,23 +272,4 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
))
}
}
@SuppressLint("ClickableViewAccessibility")
fun EditText.enableScrollText()
{
overScrollMode = View.OVER_SCROLL_ALWAYS
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
isVerticalScrollBarEnabled = true
setOnTouchListener { view, event ->
if (view is EditText) {
if(!view.text.isNullOrEmpty()) {
view.parent.requestDisallowInterceptTouchEvent(true)
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_UP -> view.parent.requestDisallowInterceptTouchEvent(false)
}
}
}
false
}
}
}

@ -4,21 +4,41 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem
class DownloadFragmentAdapter (
private val resultItem: ResultItem,
fragmentManager : FragmentManager,
lifecycle : Lifecycle,
private val fragments: List<Fragment>
private var result: ResultItem,
private var downloadItem: DownloadItem?
) : FragmentStateAdapter(fragmentManager, lifecycle) {
fun setResultItem(res: ResultItem){
result = res
}
fun setDownloadItem(dd: DownloadItem){
downloadItem = dd
}
fun setTitleAuthor(t: String, a: String){
result.title = t
result.author = a
downloadItem?.title = t
downloadItem?.author = a
}
override fun getItemCount(): Int {
return fragments.size
return 3
}
override fun createFragment(position: Int): Fragment {
return fragments[position]
return when(position){
0 -> DownloadAudioFragment(result, downloadItem)
1 -> DownloadVideoFragment(result, downloadItem)
else -> DownloadCommandFragment(result, downloadItem)
}
}
}

@ -12,23 +12,16 @@ import android.graphics.Color
import android.os.Bundle
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.get
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
@ -41,7 +34,6 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
@ -62,13 +54,11 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
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.lang.reflect.Type
import java.text.SimpleDateFormat
import java.util.Locale
@ -628,7 +618,6 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
}
override fun onClick(p0: View?) {
TODO("Not yet implemented")
}
override fun onDownloadItemUpdate(resultItemID: Long, item: DownloadItem) {

@ -6,6 +6,7 @@ import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -13,7 +14,7 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -37,7 +38,7 @@ import kotlinx.coroutines.withContext
import java.io.File
class DownloadVideoFragment(private val resultItem: ResultItem, private var currentDownloadItem: DownloadItem?) : Fragment(), TitleAuthorSync {
class DownloadVideoFragment(private var resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "") : Fragment(), GUISync {
private var fragmentView: View? = null
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
@ -76,14 +77,16 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java)
}else{
downloadViewModel.createDownloadItemFromResult(resultItem, Type.video)
downloadViewModel.createDownloadItemFromResult(resultItem, url, Type.video)
}
}
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
try {
title = view.findViewById(R.id.title_textinput)
title.editText!!.setText(downloadItem.title)
if (title.editText?.text?.isEmpty() == true){
title.editText!!.setText(downloadItem.title)
}
title.editText!!.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
@ -93,7 +96,9 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
})
author = view.findViewById(R.id.author_textinput)
author.editText!!.setText(downloadItem.author)
if (author.editText?.text?.isEmpty() == true){
author.editText!!.setText(downloadItem.author)
}
author.editText!!.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
@ -102,6 +107,18 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
}
})
if (savedInstanceState?.containsKey("updated") == true){
if (!listOf(resultItem?.title, downloadItem.title).contains(title.editText?.text.toString())){
title.endIconDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_refresh)
downloadItem.title = title.editText?.text.toString()
}
if (!listOf(resultItem?.author, downloadItem.author).contains(author.editText?.text.toString())){
author.endIconDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_refresh)
downloadItem.author = author.editText?.text.toString()
}
}
saveDir = view.findViewById(R.id.outputPath)
saveDir.editText!!.setText(
FileUtil.formatPath(downloadItem.downloadPath)
@ -113,7 +130,16 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
videoPathResultLauncher.launch(intent)
val callback = FileUtil.getURIFromResult(this@DownloadVideoFragment, requireActivity()){ result ->
downloadItem.downloadPath = result
saveDir.editText?.setText(FileUtil.formatPath(result), TextView.BufferType.EDITABLE)
val free = FileUtil.convertFileSize(
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace)
freeSpace.text = String.format( getString(R.string.freespace) + ": " + free)
if (free == "?") freeSpace.visibility = View.GONE
}
callback.launch(intent)
}
freeSpace = view.findViewById(R.id.freespace)
@ -125,7 +151,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
var formats = mutableListOf<Format>()
if (currentDownloadItem == null) {
formats.addAll(resultItem.formats)
formats.addAll(resultItem?.formats ?: listOf())
}else{
//if its updating a present downloaditem and its the wrong category
if (currentDownloadItem!!.type != Type.video){
@ -163,9 +189,11 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
}
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultItem.formats.removeAll(formats)
resultItem.formats.addAll(allFormats.first().filter { !genericVideoFormats.contains(it) })
resultViewModel.update(resultItem)
resultItem?.formats?.removeAll(formats)
resultItem?.formats?.addAll(allFormats.first().filter { !genericVideoFormats.contains(it) })
if (resultItem != null){
resultViewModel.update(resultItem!!)
}
}
}
formats = allFormats.first().filter { !genericVideoFormats.contains(it) }.toMutableList()
@ -232,7 +260,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
},
cutClicked = { cutVideoListener ->
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem.urls, resultItem.chapters, cutVideoListener)
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem?.urls ?: "", resultItem?.chapters ?: listOf(), cutVideoListener)
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
}
},
@ -271,28 +299,17 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
downloadItem.title = t
downloadItem.author = a
title.editText?.setText(t)
title.endIconDrawable = null
author.editText?.setText(a)
title.endIconDrawable = null
}
private var videoPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
downloadItem.downloadPath = result.data?.data.toString()
saveDir.editText?.setText(FileUtil.formatPath(result.data?.data.toString()), TextView.BufferType.EDITABLE)
val free = FileUtil.convertFileSize(
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace)
freeSpace.text = String.format( getString(R.string.freespace) + ": " + free)
if (free == "?") freeSpace.visibility = View.GONE
}
override fun updateUI(res: ResultItem?) {
resultItem = res
val state = Bundle()
state.putBoolean("updated", true)
onViewCreated(requireView(),savedInstanceState = state)
}
}

@ -8,6 +8,7 @@ import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import android.widget.*
import androidx.compose.runtime.invalidateGroupsWithKey
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
@ -212,24 +213,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
selectedVideo =
chosenFormats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.maxByOrNull { it.filesize }!!
}
//simple video format selection
if (items.size == 1){
listener.onFormatClick(formats, listOf(FormatTuple(selectedVideo, selectedAudios)))
}else{
//playlist format selection
val selectedFormats = mutableListOf<Format>()
formatCollection.forEach {
selectedFormats.add(it.first{ f -> f.format_id == selectedVideo.format_id})
}
if (selectedFormats.isEmpty()) {
items.forEach { _ ->
selectedFormats.add(selectedVideo)
}
}
listener.onFormatClick(formats, selectedFormats.map { FormatTuple(it, selectedAudios) })
}
returnFormats()
dismiss()
}
@ -237,9 +221,30 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
refreshBtn.performClick()
}
}
private fun returnFormats(){
//simple video format selection
if (items.size == 1){
listener.onFormatClick(formats, listOf(FormatTuple(selectedVideo, selectedAudios)))
}else{
//playlist format selection
val selectedFormats = mutableListOf<Format>()
formatCollection.forEach {
selectedFormats.add(it.first{ f -> f.format_id == selectedVideo.format_id})
}
if (selectedFormats.isEmpty()) {
items.forEach { _ ->
selectedFormats.add(selectedVideo)
}
}
listener.onFormatClick(formats, selectedFormats.map { FormatTuple(it, selectedAudios) })
}
}
private fun addFormatsToView(){
//sort
val finalFormats: List<Format> = when(sortBy){
var finalFormats: List<Format> = when(sortBy){
FormatSorting.container -> chosenFormats.groupBy { it.container }.flatMap { it.value }
FormatSorting.id -> chosenFormats.sortedBy { it.format_id }
FormatSorting.codec -> {
@ -276,6 +281,14 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
}
}
if (finalFormats.isEmpty()){
finalFormats = if (items.first()?.type == Type.audio){
infoUtil.getGenericAudioFormats(requireContext().resources)
}else{
infoUtil.getGenericVideoFormats(requireContext().resources)
}
}
for (i in 0.. finalFormats.lastIndex){
val format = finalFormats[i]
val formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null)
@ -287,7 +300,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
val clickedCard = (clickedformat as MaterialCardView)
if (format.vcodec.isNotBlank() && format.vcodec != "none") {
if (clickedCard.isChecked) {
listener.onFormatClick(formats, listOf(FormatTuple(format, null)))
returnFormats()
dismiss()
}
videoFormatList.forEach { (it as MaterialCardView).isChecked = false }

@ -0,0 +1,8 @@
package com.deniscerri.ytdlnis.ui.downloadcard
import com.deniscerri.ytdlnis.database.models.ResultItem
interface GUISync {
fun updateTitleAuthor(t: String, a: String)
fun updateUI(res: ResultItem?)
}

@ -20,6 +20,7 @@ import android.view.ViewGroup
import android.view.Window
import android.widget.*
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
@ -87,7 +88,7 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity().applicationContext)
infoUtil = InfoUtil(requireActivity())
notificationUtil = NotificationUtil(requireActivity().applicationContext)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
@ -229,15 +230,14 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
downloadThumb.setOnClickListener {
downloadManager.enqueue(
DownloadManager.Request(Uri.parse(item.thumb))
DownloadManager.Request(item.thumb.toUri())
.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"))
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/" + item.title + ".jpg"))
}
title.text = item.title
@ -293,12 +293,14 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
private fun onButtonClick(type: DownloadViewModel.Type){
this.dismiss()
if (sharedPreferences.getBoolean("download_card", true)) {
val bottomSheet = DownloadBottomSheetDialog(item, type, null, false)
val bottomSheet = DownloadBottomSheetDialog(item, type)
bottomSheet.show(parentFragmentManager, "downloadSingleSheet")
} else {
lifecycleScope.launch{
val downloadItem = withContext(Dispatchers.IO){
downloadViewModel.createDownloadItemFromResult(item, type)
downloadViewModel.createDownloadItemFromResult(
result = item,
givenType = type)
}
downloadViewModel.queueDownloads(listOf(downloadItem))
}

@ -164,13 +164,14 @@ class SelectPlaylistItemsDialog(private val items: List<ResultItem?>, private va
val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) }
if (checkedResultItems.size == 1){
val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url)
val bottomSheet = DownloadBottomSheetDialog(resultItem, type, null, false)
val bottomSheet = DownloadBottomSheetDialog(resultItem, type)
bottomSheet.show(parentFragmentManager, "downloadSingleSheet")
}else{
val downloadItems = mutableListOf<DownloadItem>()
checkedResultItems.forEach { c ->
c!!.id = 0
val i = downloadViewModel.createDownloadItemFromResult(c,type)
val i = downloadViewModel.createDownloadItemFromResult(
result = c, givenType = type)
if (type == DownloadViewModel.Type.command){
i.format = downloadViewModel.getLatestCommandTemplateAsFormat()
}
@ -207,7 +208,7 @@ class SelectPlaylistItemsDialog(private val items: List<ResultItem?>, private va
}
toolbar.setNavigationOnClickListener {
requireActivity().finishAffinity()
dismiss()
}
}
@ -224,9 +225,9 @@ class SelectPlaylistItemsDialog(private val items: List<ResultItem?>, private va
private fun cleanup(){
kotlin.runCatching {
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("downloadPlaylistSheet")!!).commit()
if (parentFragmentManager.fragments.size == 1){
(activity as ShareActivity).finish()
}
}
if (activity is ShareActivity){
(activity as ShareActivity).finishAffinity()
}
}

@ -1,5 +0,0 @@
package com.deniscerri.ytdlnis.ui.downloadcard
interface TitleAuthorSync {
fun updateTitleAuthor(t: String, a: String)
}

@ -80,23 +80,25 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
workManager.cancelAllWorkByTag("download")
pauseResume.isEnabled = false
// pause active
// pause queued
withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
downloadViewModel.getQueued()
}.forEach {
cancelItem(it.id.toInt())
it.status = DownloadRepository.Status.Paused.toString()
it.status = DownloadRepository.Status.QueuedPaused.toString()
downloadViewModel.updateDownload(it)
}
// pause queued
// pause active
withContext(Dispatchers.IO){
downloadViewModel.getQueued()
downloadViewModel.getActiveDownloads()
}.forEach {
it.status = DownloadRepository.Status.QueuedPaused.toString()
cancelItem(it.id.toInt())
it.status = DownloadRepository.Status.Paused.toString()
downloadViewModel.updateDownload(it)
}
activeDownloads.notifyDataSetChanged()
pauseResume.isEnabled = true
}

@ -127,7 +127,11 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
}
},
longClickDownloadButton = {
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(it), it.type, it, false)
val sheet = DownloadBottomSheetDialog(
currentDownloadItem = it,
result = downloadViewModel.createResultItemFromDownload(it),
type = it.type
)
sheet.show(parentFragmentManager, "downloadSingleSheet")
},
scheduleButtonClick = {}

@ -99,6 +99,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
downloadViewModel.getItemByID(itemID)
}
if (item.logID != null) {
actionMode?.finish()
val bundle = Bundle()
bundle.putLong("logID", item.logID!!)
findNavController().navigate(
@ -129,7 +130,11 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
}
},
longClickDownloadButton = {
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(it), it.type, it, false)
val sheet = DownloadBottomSheetDialog(
currentDownloadItem = it,
result = downloadViewModel.createResultItemFromDownload(it),
type = it.type
)
sheet.show(parentFragmentManager, "downloadSingleSheet")
},
scheduleButtonClick = {}

@ -527,7 +527,9 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
redownload.setOnLongClickListener {
bottomSheet!!.cancel()
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromHistory(item), item.type, downloadViewModel.createDownloadItemFromHistory(item), false)
val sheet = DownloadBottomSheetDialog(
result = downloadViewModel.createResultItemFromHistory(item),
type = item.type)
sheet.show(parentFragmentManager, "downloadSingleSheet")
true
}
@ -669,7 +671,9 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
ItemTouchHelper.RIGHT -> {
val item = historyList!![position]!!
historyAdapter!!.notifyItemChanged(position)
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromHistory(item), item.type, downloadViewModel.createDownloadItemFromHistory(item), false)
val sheet = DownloadBottomSheetDialog(type = item.type,
result = downloadViewModel.createResultItemFromHistory(item)
)
sheet.show(parentFragmentManager, "downloadSingleSheet")
}
}

@ -150,7 +150,11 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
}
},
longClickDownloadButton = {
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(it), it.type, it, false)
val sheet = DownloadBottomSheetDialog(
currentDownloadItem = it,
result = downloadViewModel.createResultItemFromDownload(it),
type = it.type
)
sheet.show(parentFragmentManager, "downloadSingleSheet")
},
scheduleButtonClick = {downloadItem ->

@ -127,7 +127,11 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
}
},
longClickDownloadButton = { it: DownloadItem ->
val sheet = DownloadBottomSheetDialog(downloadViewModel.createResultItemFromDownload(it), it.type, it, false)
val sheet = DownloadBottomSheetDialog(
currentDownloadItem = it,
result = downloadViewModel.createResultItemFromDownload(it),
type = it.type
)
sheet.show(parentFragmentManager, "downloadSingleSheet")
},
scheduleButtonClick = {}

@ -133,7 +133,6 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
}
override fun onSelected(commandTemplate: CommandTemplate) {
TODO("Not yet implemented")
}
override fun onDelete(commandTemplate: CommandTemplate) {

@ -187,7 +187,6 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
}
override fun onSelected(cookie: CookieItem) {
TODO("Not yet implemented")
}
override fun onDelete(cookie: CookieItem) {

@ -19,15 +19,12 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.more.settings.SettingsActivity
import com.deniscerri.ytdlnis.ui.more.terminal.TerminalActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.system.exitProcess
class MoreFragment : Fragment() {

@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.ui.more.settings
import android.os.Bundle
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import com.deniscerri.ytdlnis.R
@ -10,9 +11,23 @@ class ProcessingSettingsFragment : BaseSettingsFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.processing_preferences, rootKey)
val preferredVideoCodec : ListPreference? = findPreference("video_codec")
val preferredAudioCodec : ListPreference? = findPreference("audio_codec")
val preferredFormatID : EditTextPreference? = findPreference("format_id")
val preferredFormatIDAudio : EditTextPreference? = findPreference("format_id_audio")
preferredVideoCodec?.summary = preferredVideoCodec?.entry
preferredVideoCodec?.setOnPreferenceChangeListener { preference, newValue ->
preferredVideoCodec.summary = preferredVideoCodec.entries[preferredVideoCodec.findIndexOfValue(newValue.toString())]
true
}
preferredAudioCodec?.summary = preferredAudioCodec?.entry
preferredAudioCodec?.setOnPreferenceChangeListener { preference, newValue ->
preferredAudioCodec.summary = preferredAudioCodec.entries[preferredAudioCodec.findIndexOfValue(newValue.toString())]
true
}
preferredFormatID?.title = "${getString(R.string.preferred_format_id)} [${getString(R.string.video)}]"
preferredFormatID?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"

@ -0,0 +1,94 @@
package com.deniscerri.ytdlnis.ui.more.terminal
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.bundleOf
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavArgument
import androidx.navigation.NavGraph
import androidx.navigation.NavType
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navArgument
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.viewmodel.TerminalViewModel
import com.deniscerri.ytdlnis.ui.BaseActivity
import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.properties.Delegates
class TerminalActivity : BaseActivity() {
private lateinit var terminalViewModel: TerminalViewModel
private lateinit var navHostFragment: NavHostFragment
private var downloadID by Delegates.notNull<Long>()
private lateinit var graph: NavGraph
@SuppressLint("SetTextI18n")
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_terminal)
terminalViewModel = ViewModelProvider(this)[TerminalViewModel::class.java]
downloadID = savedInstanceState?.getLong("downloadID") ?: 0L
handleIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
private fun handleIntent(intent: Intent) {
val action = intent.action
val type = intent.type
Log.e(TAG, "$action $type")
var text : String? = null
if (action == Intent.ACTION_SEND && type != null) {
Log.e(TAG, action)
text = if (intent.getStringExtra(Intent.EXTRA_TEXT) == null){
val uri = if (Build.VERSION.SDK_INT >= 33){
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
}else{
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
"-a \"${FileUtil.formatPath(uri?.path ?: "")}\""
}else{
intent.getStringExtra(Intent.EXTRA_TEXT)
}
}
navHostFragment = supportFragmentManager.findFragmentById(R.id.frame_layout) as NavHostFragment
graph = navHostFragment.navController.navInflater.inflate(R.navigation.terminal_graph)
lifecycleScope.launch {
val count = withContext(Dispatchers.IO){
terminalViewModel.getCount()
}
val bundle = Bundle()
if (count == 0){
bundle.putString("share", text ?: "")
graph.setStartDestination(R.id.terminalFragment)
graph.findNode(graph.startDestinationId)?.addArgument("share", NavArgument.Builder()
.setType(NavType.StringType)
.setDefaultValue(text ?: "")
.build()
)
}
navHostFragment.navController.setGraph(graph, bundle)
}
}
companion object {
private const val TAG = "TerminalActivity"
}
}

@ -0,0 +1,114 @@
package com.deniscerri.ytdlnis.ui.more.terminal
import android.annotation.SuppressLint
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.addCallback
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.forEach
import androidx.core.view.get
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.TerminalDownloadsAdapter
import com.deniscerri.ytdlnis.database.models.TerminalItem
import com.deniscerri.ytdlnis.database.viewmodel.TerminalViewModel
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.progressindicator.LinearProgressIndicator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnItemClickListener {
private var topAppBar: MaterialToolbar? = null
private lateinit var terminalViewModel: TerminalViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
terminalViewModel = ViewModelProvider(this)[TerminalViewModel::class.java]
return inflater.inflate(R.layout.fragment_terminal_download_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launch {
val recycler = view.findViewById<RecyclerView>(R.id.terminal_recycler)
val adapter = TerminalDownloadsAdapter(this@TerminalDownloadsListFragment, requireActivity())
recycler.adapter = adapter
recycler.enableFastScroll()
recycler.layoutManager = GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_size))
topAppBar = requireActivity().findViewById(R.id.custom_command_toolbar)
topAppBar!!.setNavigationOnClickListener { requireActivity().finish() }
topAppBar?.menu?.forEach { it.isVisible = false }
topAppBar?.menu?.get(0)?.isVisible = true
topAppBar?.setOnMenuItemClickListener { m: MenuItem ->
when(m.itemId){
R.id.add -> {
findNavController().navigate(R.id.terminalFragment)
}
}
true
}
terminalViewModel.getTerminals().collectLatest {
adapter.submitList(it)
if (it.isEmpty()){
findNavController().navigate(R.id.terminalFragment)
}
}
}
WorkManager.getInstance(requireContext())
.getWorkInfosByTagLiveData("terminal")
.observe(viewLifecycleOwner){ list ->
list.forEach {work ->
if (work == null) return@forEach
val id = work.progress.getInt("id", 0)
if(id == 0) return@forEach
val progress = work.progress.getInt("progress", 0)
val output = work.progress.getString("output")
val progressBar = view.findViewWithTag<LinearProgressIndicator>("$id##progress")
val outputText = view.findViewWithTag<TextView>("$id##output")
requireActivity().runOnUiThread {
kotlin.runCatching {
progressBar?.setProgressCompat(progress, true)
outputText?.text = output
}
}
}
}
}
override fun onCancelClick(itemID: Long) {
terminalViewModel.cancelTerminalDownload(itemID)
}
override fun onCardClick(item: TerminalItem) {
val bundle = Bundle()
bundle.putLong("id", item.id)
findNavController().navigate(R.id.terminalFragment, bundle)
}
}

@ -1,17 +1,17 @@
package com.deniscerri.ytdlnis.ui.more
package com.deniscerri.ytdlnis.ui.more.terminal
import android.app.ActionBar.LayoutParams
import android.app.Activity
import android.content.ClipboardManager
import android.content.Context
import android.content.Context.CLIPBOARD_SERVICE
import android.content.Context.INPUT_METHOD_SERVICE
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PersistableBundle
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
@ -23,22 +23,27 @@ import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.forEach
import androidx.core.view.get
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.TerminalItem
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.ui.BaseActivity
import com.deniscerri.ytdlnis.database.viewmodel.TerminalViewModel
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableTextHighlight
import com.deniscerri.ytdlnis.work.TerminalDownloadWorker
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -46,13 +51,13 @@ import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import kotlin.properties.Delegates
class TerminalActivity : BaseActivity() {
class TerminalFragment : Fragment() {
private var topAppBar: MaterialToolbar? = null
private lateinit var notificationUtil: NotificationUtil
private lateinit var terminalViewModel: TerminalViewModel
private var output: TextView? = null
private var input: EditText? = null
private var fab: ExtendedFloatingActionButton? = null
@ -60,35 +65,68 @@ class TerminalActivity : BaseActivity() {
private lateinit var bottomAppBar: BottomAppBar
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private lateinit var sharedPreferences: SharedPreferences
private var downloadID by Delegates.notNull<Int>()
private lateinit var downloadFile : File
private var downloadID by Delegates.notNull<Long>()
private lateinit var imm : InputMethodManager
private lateinit var metrics: DisplayMetrics
var context: Context? = null
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_terminal)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
terminalViewModel = ViewModelProvider(this)[TerminalViewModel::class.java]
downloadID = 0
return inflater.inflate(R.layout.fragment_terminal, container, false)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("input", input?.text.toString())
outState.putString("output", output?.text.toString())
outState.putBoolean("run", fab!!.text == getString(R.string.run_command))
outState.putLong("downloadID", downloadID)
}
override fun onResume() {
arguments?.remove("id")
arguments?.remove("share")
super.onResume()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var bundle = savedInstanceState
imm = requireActivity().getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
scrollView = view.findViewById(R.id.custom_command_scrollview)
topAppBar = requireActivity().findViewById(R.id.custom_command_toolbar)
topAppBar!!.setNavigationOnClickListener { requireActivity().finish() }
downloadID = savedInstanceState?.getInt("downloadID") ?: (System.currentTimeMillis().toInt() % 100000)
downloadFile = File(cacheDir.absolutePath + "/$downloadID.txt")
if (! downloadFile.exists()) downloadFile.createNewFile()
imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
input = view.findViewById(R.id.command_edittext)
context = baseContext
scrollView = findViewById(R.id.custom_command_scrollview)
topAppBar = findViewById(R.id.custom_command_toolbar)
topAppBar!!.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
if (arguments?.getLong("id") != null){
downloadID = requireArguments().getLong("id")
if(downloadID != 0L){
input!!.visibility = View.GONE
}
}
if (arguments?.containsKey("share") == true){
if (bundle == null){
bundle = Bundle()
}
bundle.putString("input", arguments?.getString("share"))
}
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
metrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(metrics)
requireActivity().windowManager.defaultDisplay.getMetrics(metrics)
bottomAppBar = findViewById(R.id.bottomAppBar)
bottomAppBar = view.findViewById(R.id.bottomAppBar)
var templateCount = 0
var shortcutCount = 0
lifecycleScope.launch {
templateCount = withContext(Dispatchers.IO){
commandTemplateViewModel.getTotalNumber()
@ -112,10 +150,10 @@ class TerminalActivity : BaseActivity() {
when(it.itemId){
R.id.command_templates -> {
if (templateCount == 0){
Toast.makeText(context, getString(R.string.add_template_first), Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), getString(R.string.add_template_first), Toast.LENGTH_SHORT).show()
}else{
lifecycleScope.launch {
UiUtil.showCommandTemplates(this@TerminalActivity, commandTemplateViewModel){ templates ->
UiUtil.showCommandTemplates(requireActivity(), commandTemplateViewModel){ templates ->
templates.forEach {c ->
input!!.text.insert(input!!.selectionStart, c.content + " ")
}
@ -130,9 +168,14 @@ class TerminalActivity : BaseActivity() {
R.id.shortcuts -> {
lifecycleScope.launch {
if (shortcutCount > 0){
UiUtil.showShortcuts(this@TerminalActivity, commandTemplateViewModel){sh ->
input!!.text.insert(input!!.selectionStart, "$sh ")
}
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel,
itemSelected = {sh ->
input!!.setText("${input!!.text} $sh")
},
itemRemoved = {removed ->
input!!.setText(input!!.text.replace("(${Regex.escape(removed)})(?!.*\\1)".toRegex(), "").trimEnd())
input!!.setSelection(input!!.text.length)
})
}
}
}
@ -148,82 +191,55 @@ class TerminalActivity : BaseActivity() {
true
}
output = findViewById(R.id.custom_command_output)
output = view.findViewById(R.id.custom_command_output)
output!!.setTextIsSelectable(true)
output!!.layoutParams!!.width = LayoutParams.WRAP_CONTENT
input = findViewById(R.id.command_edittext)
input!!.requestFocus()
fab = findViewById(R.id.command_fab)
fab = view.findViewById(R.id.command_fab)
fab!!.setOnClickListener {
if (fab!!.text == getString(R.string.run_command)){
input!!.visibility = View.GONE
output!!.text = "${output!!.text}\n~ $ ${input!!.text}\n"
showCancelFab()
imm.hideSoftInputFromWindow(input?.windowToken, 0)
startDownload(
input!!.text.toString()
)
lifecycleScope.launch {
val command = input!!.text.toString().replace("yt-dlp", "")
downloadID = withContext(Dispatchers.IO){
terminalViewModel.insert(TerminalItem(command = command))
}
terminalViewModel.startTerminalDownloadWorker(TerminalItem(downloadID, command))
runWorkerListener()
}
}else {
cancelDownload()
terminalViewModel.cancelTerminalDownload(downloadID)
input!!.visibility = View.VISIBLE
hideCancelFab()
}
}
notificationUtil = NotificationUtil(this)
handleIntent(intent)
WorkManager.getInstance(this)
.getWorkInfosForUniqueWorkLiveData(downloadID.toString())
.observe(this){ list ->
list.forEach {work ->
if (work.state == WorkInfo.State.SUCCEEDED || work.state == WorkInfo.State.FAILED || work.state == WorkInfo.State.CANCELLED) {
input!!.setText("yt-dlp ")
input!!.visibility = View.VISIBLE
input!!.requestFocus()
input!!.setSelection(input!!.text.length)
hideCancelFab()
downloadFile.writeText("")
}
val outputData = work.progress.getString("output")
if (!outputData.isNullOrBlank()){
runOnUiThread{
try {
output!!.append("\n$outputData")
output!!.scrollTo(0, output!!.height)
scrollView!!.fullScroll(View.FOCUS_DOWN)
}catch (ignored: Exception) {}
}
}
}
}
notificationUtil = NotificationUtil(requireContext())
initMenu()
input?.enableTextHighlight()
output?.enableTextHighlight()
input?.setText(savedInstanceState?.getString("input") ?: input?.text)
input?.append(bundle?.getString("input") ?: "")
input!!.requestFocus()
input!!.setSelection(input!!.text.length)
output?.text = savedInstanceState?.getString("output") ?: output?.text
if (savedInstanceState?.getBoolean("run") == true){
output?.text = bundle?.getString("output") ?: output?.text
if (bundle?.getBoolean("run") == true){
showCancelFab()
}
}
override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
super.onSaveInstanceState(outState)
outState.putString("input", input?.text.toString())
outState.putString("output", output?.text.toString())
outState.putBoolean("run", fab!!.text == getString(R.string.run_command))
outState.putInt("downloadID", downloadID)
runWorkerListener()
}
private fun initMenu() {
topAppBar?.menu?.get(0)?.isVisible = false
topAppBar?.menu?.get(1)?.isVisible = true
topAppBar?.menu?.get(2)?.isVisible = true
topAppBar?.setOnMenuItemClickListener { m: MenuItem ->
when(m.itemId){
R.id.wrap -> {
var scrollView = findViewById<HorizontalScrollView>(R.id.horizontalscroll_output)
var scrollView = requireView().findViewById<HorizontalScrollView>(R.id.horizontalscroll_output)
if(scrollView != null){
val parent = (scrollView.parent as ViewGroup)
scrollView.removeAllViews()
@ -232,7 +248,7 @@ class TerminalActivity : BaseActivity() {
}else{
val parent = output?.parent as ViewGroup
parent.removeView(output)
scrollView = HorizontalScrollView(this)
scrollView = HorizontalScrollView(requireContext())
scrollView.layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
@ -244,7 +260,7 @@ class TerminalActivity : BaseActivity() {
}
R.id.export_clipboard -> {
lifecycleScope.launch(Dispatchers.IO){
val clipboard: ClipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val clipboard: ClipboardManager = requireActivity().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setText(output?.text)
}
}
@ -252,39 +268,6 @@ class TerminalActivity : BaseActivity() {
true
}
}
override fun onDestroy() {
downloadFile.delete()
super.onDestroy()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
private fun handleIntent(intent: Intent) {
val action = intent.action
val type = intent.type
Log.e(TAG, "$action $type")
if (action == Intent.ACTION_SEND && type != null) {
Log.e(TAG, action)
val text = if (intent.getStringExtra(Intent.EXTRA_TEXT) == null){
val uri = if (Build.VERSION.SDK_INT >= 33){
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
}else{
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
"-a \"${FileUtil.formatPath(uri?.path ?: "")}\""
}else{
intent.getStringExtra(Intent.EXTRA_TEXT)
}
val txt = "yt-dlp $text"
input!!.setText(txt)
}
}
private fun hideCancelFab() {
fab!!.text = getString(R.string.run_command)
fab!!.setIconResource(R.drawable.ic_baseline_keyboard_arrow_right_24)
@ -299,7 +282,7 @@ class TerminalActivity : BaseActivity() {
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
contentResolver?.takePersistableUriPermission(
requireActivity().contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
@ -309,34 +292,43 @@ class TerminalActivity : BaseActivity() {
}
}
private fun startDownload(command: String?) {
val cmd = if (command!!.contains("yt-dlp")) command.replace("yt-dlp", "")
else command
val workRequest = OneTimeWorkRequestBuilder<TerminalDownloadWorker>()
.setInputData(
Data.Builder()
.putInt("id", downloadID)
.putString("command", cmd)
.build()
)
.addTag("terminal")
.build()
private fun runWorkerListener(){
WorkManager.getInstance(this).beginUniqueWork(
downloadID.toString(),
ExistingWorkPolicy.KEEP,
workRequest
).enqueue()
}
WorkManager.getInstance(requireContext())
.getWorkInfosByTagLiveData(downloadID.toString())
.observe(viewLifecycleOwner){ list ->
list.forEach {work ->
if (listOf(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED).contains(work.state)) {
requireActivity().runOnUiThread {
kotlin.runCatching {
input!!.setText("yt-dlp ")
input!!.visibility = View.VISIBLE
input!!.requestFocus()
input!!.setSelection(input!!.text.length)
hideCancelFab()
}
}
return@forEach
}
val outputData = work.progress.getString("output")
if (!outputData.isNullOrBlank()){
val pieces = output!!.text.split("\n").toMutableList()
val lastLine = pieces.last()
requireActivity().runOnUiThread{
kotlin.runCatching {
showCancelFab()
if (listOf(lastLine, outputData).all { it.contains("[download]") }){
output!!.text = pieces.dropLast(1).joinToString("\n")
}
output!!.append("\n$outputData")
output!!.scrollTo(0, output!!.height)
scrollView!!.fullScroll(View.FOCUS_DOWN)
}
}
}
private fun cancelDownload() {
YoutubeDL.getInstance().destroyProcessById(downloadID.toString())
WorkManager.getInstance(this).cancelAllWorkByTag(downloadID.toString())
notificationUtil.cancelDownloadNotification(downloadID)
}
}
}
companion object {
private const val TAG = "CustomCommandActivity"
}
}

@ -1,7 +1,9 @@
package com.deniscerri.ytdlnis.util
import android.app.Activity
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.media.MediaScannerConnection
import android.net.Uri
@ -10,15 +12,21 @@ import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.util.Log
import android.view.View
import android.webkit.MimeTypeMap
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.copyFolderTo
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.moveFileTo
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -250,7 +258,11 @@ object FileUtil {
fun scanMedia(files: List<String>, context: Context) : List<String> {
try {
val paths = files.sortedByDescending { File(it).length() }
runCatching {MediaScannerConnection.scanFile(context, paths.toTypedArray(), null, null) }
runCatching {
paths.forEach {
MediaScannerConnection.scanFile(context, arrayOf(it), null, null)
}
}
return paths
}catch (e: Exception){
e.printStackTrace()
@ -296,4 +308,20 @@ object FileUtil {
val symbols = DecimalFormatSymbols(Locale.US)
return "${DecimalFormat("#,##0.#", symbols).format(s / 1024.0.pow(digitGroups.toDouble()))} ${units[digitGroups]}"
}
fun getURIFromResult(f: Fragment, a: FragmentActivity?, callback: (data: String) -> Unit) = f.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
a?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
callback(result.data?.data.toString())
}
}
}

@ -1,25 +1,49 @@
package com.deniscerri.ytdlnis.util
import android.app.Activity
import android.app.DownloadManager
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Looper
import android.text.Html
import android.util.Log
import android.util.Patterns
import android.util.TypedValue
import android.view.WindowManager
import android.widget.Toast
import androidx.navigation.findNavController
import androidx.preference.AndroidResources
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.ErrorDialogActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
@ -48,20 +72,14 @@ class InfoUtil(private val context: Context) {
fun search(query: String): ArrayList<ResultItem?> {
return when(sharedPreferences.getString("search_engine", "ytsearch")){
"ytsearch" -> try{
searchFromPiped(query)
}catch (e: Exception){
getFromYTDL(query)
}
"ytsearchmusic" -> try{
searchFromPipedMusic(query)
}catch (e: Exception){
getFromYTDL(query)
return runCatching {
when(sharedPreferences.getString("search_engine", "ytsearch")){
"ytsearch" -> searchFromPiped(query)
"ytsearchmusic" -> searchFromPipedMusic(query)
else -> throw Exception()
}
else -> getFromYTDL(query)
}.getOrElse {
getFromYTDL(query)
}
}
@ -576,11 +594,9 @@ class InfoUtil(private val context: Context) {
}
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
request.addOption("--proxy", proxy)
}
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
@ -622,7 +638,7 @@ class InfoUtil(private val context: Context) {
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
}
}
val website = if (jsonObject.has("ie_key")) jsonObject.getString("ie_key") else jsonObject.getString("extractor")
val website = jsonObject.getString(listOf("ie_key", "extractor_key", "extractor").first { jsonObject.has(it) })
var playlistTitle: String? = ""
if (jsonObject.has("playlist_title")) playlistTitle = jsonObject.getString("playlist_title")
if(playlistTitle.equals(query)) playlistTitle = ""
@ -673,15 +689,17 @@ class InfoUtil(private val context: Context) {
website,
playlistTitle!!,
formats,
urls,
urls,
chapters
)
)
}
} catch (e: Exception) {
Looper.prepare().run {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
val intent = Intent(context, ErrorDialogActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.putExtra("title", context.resources.getString(R.string.no_results))
intent.putExtra("message", e.message)
context.startActivity(intent)
e.printStackTrace()
}
return items
@ -975,21 +993,25 @@ class InfoUtil(private val context: Context) {
if (aria2) {
request.addOption("--downloader", "libaria2c.so")
request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"")
} else {
val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1)
if (concurrentFragments > 1) request.addOption("-N", concurrentFragments)
}
val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1)
if (concurrentFragments > 1) request.addOption("-N", concurrentFragments)
val retries = sharedPreferences.getString("--retries", "")!!
val fragmentRetries = sharedPreferences.getString("--fragment_retries", "")!!
if(retries.isNotEmpty()) request.addOption("retries", retries)
if(fragmentRetries.isNotEmpty()) request.addOption("fragment-retries", fragmentRetries)
if(retries.isNotEmpty()) request.addOption("--retries", retries)
if(fragmentRetries.isNotEmpty()) request.addOption("--fragment-retries", fragmentRetries)
val limitRate = sharedPreferences.getString("limit_rate", "")!!
if (limitRate.isNotBlank()) request.addOption("-r", limitRate)
val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!!
if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL)
val limitRate = sharedPreferences.getString("limit_rate", "")
if (limitRate != "") request.addOption("-r", limitRate!!)
if(downloadItem.type != DownloadViewModel.Type.command){
request.addOption("--trim-filenames", 120)
request.addOption("--trim-filenames", downDir.absolutePath.length + 120)
if (downloadItem.SaveThumb) {
request.addOption("--write-thumbnail")
@ -1175,10 +1197,7 @@ class InfoUtil(private val context: Context) {
}
request.addOption("--embed-chapters")
}
if (downloadItem.videoPreferences.embedSubs) {
request.addOption("--embed-subs")
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" })
}
var cont = ""
val outputContainer = downloadItem.container
@ -1302,14 +1321,19 @@ class InfoUtil(private val context: Context) {
val subFormat = sharedPreferences.getString("sub_format", "srt")
request.addOption("--write-subs")
request.addOption("--write-auto-subs")
if(subFormat!!.isNotBlank()){
request.addOption("--sub-format", "${subFormat}/best")
request.addOption("--convert-subtitles", subFormat ?: "srt")
}
if (!downloadItem.videoPreferences.embedSubs) {
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" })
}
}
if (downloadItem.videoPreferences.embedSubs) {
request.addOption("--embed-subs")
}
if (downloadItem.videoPreferences.embedSubs || downloadItem.videoPreferences.writeSubs){
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" })
request.addOption("--write-auto-subs")
}
if (downloadItem.videoPreferences.removeAudio){

@ -91,11 +91,14 @@ object UiUtil {
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 = formatNote.uppercase()
formatCard.findViewById<TextView>(R.id.format_note).text = formatNote.uppercase()
val audioFormatsTextView = formatCard.findViewById<TextView>(R.id.audio_formats)
if (!audioFormats.isNullOrEmpty()) {
audioFormatsTextView.text = audioFormats.joinToString("+") { it.format_id }
audioFormatsTextView.visibility = View.VISIBLE
}else{
val title = "${formatNote.uppercase()} + [${audioFormats.joinToString("/") { "(${it.format_id}) ${it.format_note}" }}]"
formatCard.findViewById<TextView>(R.id.format_note).text = title
audioFormatsTextView.visibility = View.GONE
}
formatCard.findViewById<TextView>(R.id.format_id).text = "id: ${chosenFormat.format_id}"
val codec =
@ -729,14 +732,18 @@ object UiUtil {
bottomSheet.cancel()
}
bottomSheet.setOnShowListener {
val behavior = bottomSheet.behavior
val displayMetrics = DisplayMetrics()
activity.windowManager.defaultDisplay.getMetrics(displayMetrics)
behavior.peekHeight = displayMetrics.heightPixels / 2
}
bottomSheet.show()
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
suspend fun showShortcuts(activity: Activity, commandTemplateViewModel: CommandTemplateViewModel, itemSelected: (itemSelected: String) -> Unit){
suspend fun showShortcuts(activity: Activity, commandTemplateViewModel: CommandTemplateViewModel, itemSelected: (itemSelected: String) -> Unit, itemRemoved: (itemRemoved: String) -> Unit){
val bottomSheet = BottomSheetDialog(activity)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.template_shortcuts_list)
@ -751,16 +758,24 @@ object UiUtil {
val chip = activity.layoutInflater.inflate(R.layout.suggestion_chip, chipGroup, false) as Chip
chip.text = shortcut.content
chip.setOnClickListener {
itemSelected(shortcut.content)
if (chip.isChecked){
itemSelected(shortcut.content)
}else{
itemRemoved(shortcut.content)
}
}
chipGroup.addView(chip)
}
bottomSheet.setOnShowListener {
val behavior = bottomSheet.behavior
val displayMetrics = DisplayMetrics()
activity.windowManager.defaultDisplay.getMetrics(displayMetrics)
behavior.peekHeight = displayMetrics.heightPixels / 3
}
bottomSheet.show()
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
fun configureVideo(
@ -913,7 +928,7 @@ object UiUtil {
val inputLayout = context.layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = context.getString(R.string.file_name_template)
if (items.size == 1){
if (items.size == 1 || items.all { it.customFileNameTemplate == items[0].customFileNameTemplate }){
editText.setText(items[0].customFileNameTemplate)
}
editText.setSelection(editText.text.length)
@ -936,7 +951,6 @@ object UiUtil {
editText.requestFocus()
imm.showSoftInput(editText, 0)
}, 300)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
@ -1013,7 +1027,7 @@ object UiUtil {
val inputLayout = context.layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = context.getString(R.string.file_name_template)
if (items.size == 1){
if (items.size == 1 || items.all { it.customFileNameTemplate == items[0].customFileNameTemplate }){
editText.setText(items[0].customFileNameTemplate)
}
editText.setSelection(editText.text.length)
@ -1036,7 +1050,6 @@ object UiUtil {
editText.requestFocus()
imm.showSoftInput(editText, 0)
}, 300)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}

@ -77,8 +77,8 @@ class UpdateUtil(var context: Context) {
.setTitle(v.tag_name)
.setMessage(v.body)
.setIcon(R.drawable.ic_update_app)
.setNegativeButton("Cancel") { _: DialogInterface?, _: Int -> }
.setPositiveButton("Update") { _: DialogInterface?, _: Int ->
.setNegativeButton(context.resources.getString(R.string.cancel)) { _: DialogInterface?, _: Int -> }
.setPositiveButton(context.resources.getString(R.string.update)) { _: DialogInterface?, _: Int ->
runCatching {
val releaseVersion = v.assets.firstOrNull { it.name.contains(Build.SUPPORTED_ABIS[0]) }
if (releaseVersion == null){

@ -10,7 +10,6 @@ import android.widget.Toast
import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.deniscerri.ytdlnis.R
@ -19,12 +18,13 @@ import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.more.TerminalActivity
import com.deniscerri.ytdlnis.ui.more.terminal.TerminalActivity
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -39,6 +39,7 @@ class TerminalDownloadWorker(
override suspend fun doWork(): Result {
itemId = inputData.getInt("id", 0)
val command = inputData.getString("command")
val dao = DBManager.getInstance(context).terminalDao
if (itemId == 0) return Result.failure()
if (command!!.isEmpty()) return Result.failure()
@ -59,10 +60,6 @@ class TerminalDownloadWorker(
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val downloadLocation = sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
val outputFile = File(context.cacheDir.absolutePath + "/$itemId.txt")
if (! outputFile.exists()) outputFile.createNewFile()
request.addOption(
"--config-locations",
File(context.cacheDir.absolutePath + "/config-TERMINAL[${System.currentTimeMillis()}].txt").apply {
@ -135,9 +132,9 @@ class TerminalDownloadWorker(
}
return runBlocking {
it.out.chunked(10000).forEach {
it.out.split("\n").forEach {
setProgress(workDataOf("progress" to 100, "output" to it, "id" to itemId, "log" to logDownloads))
Thread.sleep(100)
Thread.sleep(50)
}
if (logDownloads){
@ -146,13 +143,10 @@ class TerminalDownloadWorker(
}
}
notificationUtil.cancelDownloadNotification(itemId)
dao.delete(itemId.toLong())
return@runBlocking Result.success()
}
}.onFailure {
if (it is YoutubeDL.CanceledException) {
return Result.failure()
}
return runBlocking {
it.message?.chunked(10000)?.forEach {
setProgress(workDataOf("progress" to 100, "output" to it, "id" to itemId, "log" to logDownloads))
@ -169,7 +163,7 @@ class TerminalDownloadWorker(
File(FileUtil.getDefaultCommandPath() + "/" + itemId).deleteRecursively()
Log.e(TAG, context.getString(R.string.failed_download), it)
notificationUtil.cancelDownloadNotification(itemId)
dao.delete(itemId.toLong())
return@runBlocking Result.failure()
}

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="0dp"
android:layout_marginHorizontal="10dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="H,2:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/active_download_card_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="20dp"
app:cardElevation="10dp"
app:cardMaxElevation="12dp"
android:checkable="true"
app:strokeWidth="0dp"
app:cardPreventCornerOverlap="true"
android:layout_margin="10dp">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="match_parent"
app:trackColor="#000"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:alpha="0.3"
android:scaleY="200"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="10dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:paddingStart="10dp"
android:paddingTop="10dp"
android:paddingEnd="10dp"
android:shadowColor="#000"
android:fontFamily="monospace"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="2"
android:textColor="#FFF"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/active_download_stop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/active_download_stop"
style="?attr/materialIconButtonFilledStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
app:backgroundTint="?attr/colorSurface"
app:cornerRadius="15dp"
app:icon="@drawable/baseline_close_24"
app:iconSize="30dp"
app:iconTint="?android:textColorPrimary"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintBottom_toTopOf="@+id/output"
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="title,active_download_stop" />
<TextView
android:id="@+id/output"
android:layout_width="match_parent"
android:layout_height="0dp"
android:ellipsize="end"
android:focusable="true"
android:fontFamily="monospace"
android:maxLines="5"
android:padding="10dp"
android:shadowColor="#000"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="2"
android:textColor="#FFF"
android:textSize="11sp"
android:gravity="bottom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/barrier" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -1,121 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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:layout_height="match_parent"
tools:context="com.deniscerri.ytdlnis.ui.more.TerminalActivity">
android:fitsSystemWindows="true"
tools:context=".ui.more.terminal.TerminalActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:liftOnScroll="true"
android:background="@android:color/transparent"
android:fitsSystemWindows="true">
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/custom_command_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="0dp"
app:title="@string/terminal"
app:menu="@menu/terminal_top_menu"
android:layout_width="match_parent"
app:navigationIcon="@drawable/ic_back"
android:layout_height="match_parent"/>
app:title="@string/terminal" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@+id/coordinatorLayout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ScrollView
android:id="@+id/custom_command_scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusableInTouchMode="true"
android:orientation="vertical">
<HorizontalScrollView
android:id="@+id/horizontalscroll_output"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/custom_command_output"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:gravity="bottom"
android:textSize="15sp"
android:scrollbars="horizontal|vertical"
android:scrollHorizontally="true"
android:textIsSelectable="true" />
</HorizontalScrollView>
<EditText
android:id="@+id/command_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:fontFamily="monospace"
android:gravity="start"
android:inputType="textMultiLine|textNoSuggestions"
android:maxLines="100"
android:text="yt-dlp "
android:textSize="15sp" />
</LinearLayout>
</ScrollView>
</LinearLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"
style="@style/Widget.Material3.BottomAppBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:menu="@menu/terminal_menu" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/command_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/run_command"
app:icon="@drawable/ic_baseline_keyboard_arrow_right_24"
app:layout_anchor="@id/bottomAppBar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/frame_layout"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar"
app:navGraph="@navigation/terminal_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -22,12 +22,13 @@
<TextView
android:id="@+id/bottom_sheet_title"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:singleLine="false"
android:text="@string/command_templates"
android:textSize="25sp"
android:maxLines="2"
android:layout_marginEnd="10dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintEnd_toStartOf="@+id/command_ok"
app:layout_constraintStart_toStartOf="parent"

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView android:layout_width="match_parent"
<androidx.core.widget.NestedScrollView xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:scrollbars="none"
android:orientation="vertical"
android:layout_height="wrap_content"
@ -22,6 +23,29 @@
android:orientation="horizontal"
android:paddingTop="20dp">
<com.facebook.shimmer.ShimmerFrameLayout
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_marginTop="4dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/shimmer_loading_title"
android:orientation="vertical">
<TextView
android:id="@+id/bottom_sheet_loading_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:singleLine="false"
android:text="@string/loading"
android:textSize="25sp" />
</com.facebook.shimmer.ShimmerFrameLayout>
<TextView
android:id="@+id/bottom_sheet_title"
@ -37,6 +61,27 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.facebook.shimmer.ShimmerFrameLayout
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/shimmer_loading_title"
android:id="@+id/shimmer_loading_subtitle"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:singleLine="false"
android:text="@string/please_wait"
android:textSize="12sp" />
</com.facebook.shimmer.ShimmerFrameLayout>
<TextView
android:id="@+id/bottom_sheet_subtitle"
android:layout_width="wrap_content"

@ -49,82 +49,120 @@
android:id="@+id/format_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
app:layout_constraintHorizontal_bias="0.0"
android:maxLines="2"
android:paddingStart="10dp"
android:ellipsize="end"
android:gravity="start"
android:maxLines="2"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="wrap_content"
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginStart="10dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/format_id"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/format_note"
app:layout_constraintVertical_bias="1.0">
app:layout_constraintTop_toBottomOf="@id/format_note">
<TextView
android:id="@+id/codec"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Tertiary"
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_marginEnd="10dp"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
android:layout_marginStart="10dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="horizontal">
<TextView
android:id="@+id/audio_formats"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorPrimaryContainer"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textColor="@color/white"
android:textStyle="bold"
app:cornerRadius="10dp"
app:drawableStartCompat="@drawable/ic_music"
app:drawableTint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/codec"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Tertiary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</HorizontalScrollView>
</RelativeLayout>
<TextView
android:id="@+id/format_id"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="3dp"
android:clickable="false"
android:gravity="bottom|end"
android:maxLength="10"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintVertical_bias="1.0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/linearLayout"
app:layout_constraintTop_toBottomOf="@+id/format_note" />

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.deniscerri.ytdlnis.ui.more.TerminalActivity">
tools:context="com.deniscerri.ytdlnis.ui.more.terminal.TerminalActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.deniscerri.ytdlnis.ui.more.TerminalActivity">
tools:context="com.deniscerri.ytdlnis.ui.more.terminal.TerminalActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"

@ -21,6 +21,7 @@
android:paddingHorizontal="10dp"
android:paddingBottom="10dp"
android:hint="@string/title"
app:endIconMode="custom"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
@ -43,6 +44,7 @@
android:layout_height="wrap_content"
android:layout_weight="40"
android:hint="@string/author"
app:endIconMode="custom"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.deniscerri.ytdlnis.ui.more.TerminalActivity">
tools:context="com.deniscerri.ytdlnis.ui.more.terminal.TerminalActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.deniscerri.ytdlnis.ui.more.TerminalActivity">
tools:context="com.deniscerri.ytdlnis.ui.more.terminal.TerminalActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/terminal_view"
android:layout_width="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ScrollView
android:id="@+id/custom_command_scrollview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="10dp"
app:layout_constraintBottom_toTopOf="@+id/coordinatorLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusableInTouchMode="true"
android:orientation="vertical">
<HorizontalScrollView
android:id="@+id/horizontalscroll_output"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/custom_command_output"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:gravity="bottom"
android:scrollbars="horizontal|vertical"
android:scrollHorizontally="true"
android:textIsSelectable="true"
android:textSize="15sp" />
</HorizontalScrollView>
<EditText
android:id="@+id/command_edittext"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:fontFamily="monospace"
android:gravity="start"
android:inputType="textMultiLine|textNoSuggestions"
android:maxLines="10000"
android:text="yt-dlp "
android:textSize="15sp" />
</LinearLayout>
</ScrollView>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"
style="@style/Widget.Material3.BottomAppBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:menu="@menu/terminal_menu" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/command_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/run_command"
app:icon="@drawable/ic_baseline_keyboard_arrow_right_24"
app:layout_anchor="@id/bottomAppBar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/active_terminal_tasks"
android:layout_width="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/terminal_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -2,6 +2,11 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title=""
android:icon="@drawable/ic_plus"
app:showAsAction="always"
android:id="@+id/add"/>
<item android:title=""
android:icon="@drawable/baseline_wrap_text_24"
app:showAsAction="always"

@ -66,9 +66,9 @@
</fragment>
<activity
android:id="@+id/terminalActivity"
android:name="com.deniscerri.ytdlnis.ui.more.TerminalActivity"
android:name="com.deniscerri.ytdlnis.ui.more.terminal.TerminalActivity"
android:label="activity_terminal"
tools:layout="@layout/activity_terminal" />
tools:layout="@layout/fragment_terminal" />
<fragment
android:id="@+id/cookiesFragment"
android:name="com.deniscerri.ytdlnis.ui.more.CookiesFragment"

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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"
app:startDestination="@id/terminalDownloadsListFragment">
<fragment
android:id="@+id/terminalDownloadsListFragment"
android:name="com.deniscerri.ytdlnis.ui.more.terminal.TerminalDownloadsListFragment"
tools:layout="@layout/fragment_terminal_downloadList"
android:label="TerminalDownloadsListFragment" >
<action
android:id="@+id/action_terminalDownloadsListFragment_to_terminalFragment2"
app:destination="@id/terminalFragment" />
</fragment>
<fragment
android:id="@+id/terminalFragment"
android:name="com.deniscerri.ytdlnis.ui.more.terminal.TerminalFragment"
tools:layout="@layout/fragment_terminal"
android:label="TerminalFragment" >
<action
android:id="@+id/action_terminalFragment2_to_terminalDownloadsListFragment"
app:destination="@id/terminalDownloadsListFragment" />
</fragment>
</navigation>

@ -808,4 +808,17 @@
<item>avc|h264</item>
<item>hev|h265</item>
</string-array>
<string-array name="audio_codec">
<item>@string/defaultValue</item>
<item>M4A</item>
<item>OPUS</item>
</string-array>
<string-array name="audio_codec_values">
<item></item>
<item>m4a|mp4a|aac</item>
<item>opus</item>
</string-array>
</resources>

@ -233,7 +233,7 @@
<string name="finished_download_notification_channel_name">Finished Downloads</string>
<string name="finished_download_notification_channel_description">Notification for finished or faulty downloads</string>
<string name="quick_download">Quick Download</string>
<string name="quick_download_summary">Don\'t fetch data beforehand</string>
<string name="quick_download_summary">Don\'t fetch data in the download card</string>
<string name="remove_audio">Remove Audio</string>
<string name="preferred_format_id">Preferred Format ID</string>
<string name="download_now">Download Now</string>
@ -303,4 +303,8 @@
<string name="update_app_beta">Beta Releases</string>
<string name="update_app_beta_summary">Enroll in the beta program. It may contain bugs.</string>
<string name="auto">Auto</string>
<string name="preferred_video_codec">Preferred Video Codec</string>
<string name="preferred_audio_codec">Preferred Audio Codec</string>
<string name="loading">Loading</string>
<string name="sponsorblock_api_url">Sponsorblock API URL</string>
</resources>

@ -54,14 +54,12 @@
app:title="@string/preferred_download_type" />
<SeekBarPreference
app:dependency="aria2"
android:defaultValue="3"
android:icon="@drawable/ic_lines"
android:max="25"
app:key="concurrent_fragments"
app:min="1"
app:showSeekBarValue="true"
android:dependency="aria2"
app:summary="@string/concurrent_fragments_summary"
app:title="@string/concurrent_fragments" />

@ -20,6 +20,15 @@
app:summary="@string/select_sponsorblock_filtering"
app:title="SponsorBlock" />
<EditTextPreference
android:dependency="use_sponsorblock"
app:icon="@drawable/ic_money"
app:key="sponsorblock_url"
app:useSimpleSummaryProvider="true"
app:defaultValue=""
app:title="@string/sponsorblock_api_url" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
@ -145,7 +154,15 @@
android:entryValues="@array/video_codec_values"
android:icon="@drawable/ic_code"
app:key="video_codec"
app:title="@string/codec" />
app:title="@string/preferred_video_codec" />
<ListPreference
android:defaultValue=""
android:entries="@array/audio_codec"
android:entryValues="@array/audio_codec_values"
android:icon="@drawable/ic_code"
app:key="audio_codec"
app:title="@string/preferred_audio_codec" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"

@ -1,18 +1,19 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#Sun Oct 08 20:53:48 CEST 2023
android.defaults.buildfeatures.buildconfig=true
android.enableJetifier=true
android.nonFinalResIds=false
android.nonTransitiveRClass=false
android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1024M" -Dfile.encoding\=UTF-8

Loading…
Cancel
Save