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 { android {
namespace 'com.deniscerri.ytdlnis' namespace 'com.deniscerri.ytdlnis'
compileSdk 33 compileSdk 34
try { try {
def propertiesFile = rootProject.file("local.properties") def propertiesFile = rootProject.file("local.properties")
@ -128,7 +128,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:$appCompatVer" implementation "androidx.appcompat:appcompat:$appCompatVer"
implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation 'com.google.android.material:material:1.9.0-rc01' implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.3.1' implementation 'androidx.recyclerview:recyclerview:1.3.1'
implementation 'androidx.preference:preference-ktx:1.2.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> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".ui.more.TerminalActivity" android:name=".ui.more.terminal.TerminalActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:exported="true" android:exported="true"
android:label="@string/terminal" android:label="@string/terminal"
@ -107,7 +107,7 @@
<activity-alias <activity-alias
android:name=".terminalShareAlias" android:name=".terminalShareAlias"
android:targetActivity=".ui.more.TerminalActivity" android:targetActivity=".ui.more.terminal.TerminalActivity"
android:enabled="false" android:enabled="false"
android:icon="@mipmap/terminal_icon" android:icon="@mipmap/terminal_icon"
android:roundIcon="@mipmap/terminal_icon_round" android:roundIcon="@mipmap/terminal_icon_round"
@ -211,6 +211,16 @@
</intent-filter> </intent-filter>
</activity> </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 <service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false" android:enabled="false"

@ -11,9 +11,13 @@ import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager 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.NotificationUtil
import com.deniscerri.ytdlnis.util.UpdateUtil import com.deniscerri.ytdlnis.util.UpdateUtil
import com.deniscerri.ytdlnis.work.UpdateYTDLWorker import com.deniscerri.ytdlnis.work.UpdateYTDLWorker
import com.google.android.material.snackbar.Snackbar
import com.yausername.aria2c.Aria2c import com.yausername.aria2c.Aria2c
import com.yausername.ffmpeg.FFmpeg import com.yausername.ffmpeg.FFmpeg
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
@ -44,34 +48,10 @@ class App : Application() {
initLibraries() initLibraries()
val appVer = sharedPreferences.getString("version", "")!! val appVer = sharedPreferences.getString("version", "")!!
if(appVer.isEmpty() || appVer != BuildConfig.VERSION_NAME){ if(appVer.isEmpty() || appVer != BuildConfig.VERSION_NAME){
UpdateUtil(this@App).updateYoutubeDL()
sharedPreferences.edit(commit = true){ sharedPreferences.edit(commit = true){
putString("version", BuildConfig.VERSION_NAME) 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){ }catch (e: Exception){
Looper.prepare().runCatching { Looper.prepare().runCatching {
Toast.makeText(this@App, e.message, Toast.LENGTH_SHORT).show() 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.fragment.findNavController
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel 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.NavigationBarView
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.navigationrail.NavigationRailView 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.BufferedReader import java.io.BufferedReader
@ -233,6 +237,19 @@ class MainActivity : BaseActivity() {
"Queue" -> navController.navigate(R.id.downloadQueueMainFragment) "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) { override fun onSaveInstanceState(savedInstanceState: Bundle) {
@ -350,8 +367,7 @@ class MainActivity : BaseActivity() {
Toast.makeText(context, "Couldn't read file", Toast.LENGTH_LONG).show() Toast.makeText(context, "Couldn't read file", Toast.LENGTH_LONG).show()
return return
} }
val item = downloadViewModel.createEmptyResultItem(f.absolutePath) val bottomSheet = DownloadBottomSheetDialog(type = DownloadViewModel.Type.command, result = downloadViewModel.createEmptyResultItem(f.absolutePath))
val bottomSheet = DownloadBottomSheetDialog(item, DownloadViewModel.Type.command, null, true)
bottomSheet.show(supportFragmentManager, "downloadSingleSheet") bottomSheet.show(supportFragmentManager, "downloadSingleSheet")
}else{ }else{
val `is` = contentResolver.openInputStream(uri!!) 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) @TypeConverters(Converters::class)
@Database( @Database(
entities = [ResultItem::class, HistoryItem::class, DownloadItem::class, CommandTemplate::class, SearchHistoryItem::class, TemplateShortcut::class, CookieItem::class, LogItem::class], entities = [
version = 9, ResultItem::class,
HistoryItem::class,
DownloadItem::class,
CommandTemplate::class,
SearchHistoryItem::class,
TemplateShortcut::class,
CookieItem::class,
LogItem::class,
TerminalItem::class
],
version = 10,
autoMigrations = [ autoMigrations = [
AutoMigration (from = 1, to = 2), AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3), AutoMigration (from = 2, to = 3),
@ -22,7 +32,8 @@ import kotlinx.coroutines.flow.Flow
AutoMigration (from = 5, to = 6), AutoMigration (from = 5, to = 6),
AutoMigration (from = 6, to = 7), AutoMigration (from = 6, to = 7),
AutoMigration (from = 7, to = 8), AutoMigration (from = 7, to = 8),
AutoMigration (from = 8, to = 9) AutoMigration (from = 8, to = 9),
AutoMigration (from = 9, to = 10)
] ]
) )
abstract class DBManager : RoomDatabase(){ abstract class DBManager : RoomDatabase(){
@ -33,6 +44,7 @@ abstract class DBManager : RoomDatabase(){
abstract val searchHistoryDao: SearchHistoryDao abstract val searchHistoryDao: SearchHistoryDao
abstract val cookieDao: CookieDao abstract val cookieDao: CookieDao
abstract val logDao: LogDao abstract val logDao: LogDao
abstract val terminalDao: TerminalDao
companion object { companion object {
//prevents multiple instances of db getting created at the same time //prevents multiple instances of db getting created at the same time

@ -20,7 +20,10 @@ interface DownloadDao {
fun getActiveDownloadsCountFlow() : Flow<Int> fun getActiveDownloadsCountFlow() : Flow<Int>
@Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)") @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'") @Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'")
fun getActiveAndPausedDownloadsList() : List<DownloadItem> 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?>{ suspend fun search(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{
val infoUtil = InfoUtil(context) val infoUtil = InfoUtil(context)
try{ if (resetResults) deleteAll()
if (resetResults) deleteAll() val res = infoUtil.search(inputQuery)
val res = infoUtil.search(inputQuery) itemCount.value = res.size
itemCount.value = res.size res.forEach {
res.forEach { resultDao.insert(it!!)
resultDao.insert(it!!) }
} return res
return res
}catch (ignored: Exception){}
return arrayListOf()
} }
suspend fun getOne(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{ suspend fun getOne(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{
val infoUtil = InfoUtil(context) val infoUtil = InfoUtil(context)
try { val v = infoUtil.getYoutubeVideo(inputQuery)
val v = infoUtil.getYoutubeVideo(inputQuery) if (resetResults) {
if (resetResults) { deleteAll()
deleteAll() itemCount.value = v.size
itemCount.value = v.size }else{
}else{ v.forEach { it?.playlistTitle = "ytdlnis-Search" }
v.forEach { it?.playlistTitle = "ytdlnis-Search" }
}
v.forEach {
resultDao.insert(it!!)
}
return ArrayList(v)
} catch (e: Exception) {
Log.e(tag, e.toString())
} }
return arrayListOf() v.forEach {
resultDao.insert(it!!)
}
return ArrayList(v)
} }
suspend fun getPlaylist(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{ 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.File import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -84,6 +80,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
private var audioContainer: String? private var audioContainer: String?
private var videoContainer: String? private var videoContainer: String?
private var videoCodec: String? private var videoCodec: String?
private var audioCodec: String?
private val dao: DownloadDao private val dao: DownloadDao
enum class Type { enum class Type {
@ -155,6 +152,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
} }
videoCodec = sharedPreferences.getString("video_codec", "") videoCodec = sharedPreferences.getString("video_codec", "")
audioCodec = sharedPreferences.getString("audio_codec", "")
} }
fun deleteDownload(id: Long) = viewModelScope.launch(Dispatchers.IO) { 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 embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
val saveSubs = sharedPreferences.getBoolean("write_subtitles", false) val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
val addChapters = sharedPreferences.getBoolean("add_chapters", false) val addChapters = sharedPreferences.getBoolean("add_chapters", false)
@ -386,12 +386,12 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
Type.audio -> { Type.audio -> {
return cloneFormat ( return cloneFormat (
try { try {
try{ val theFormats = formats.filter { it.format_note.contains("audio", ignoreCase = true) }
formats.filter { it.format_note.contains("audio", ignoreCase = true)} val requirements: MutableList<(Format) -> Boolean> = mutableListOf()
.firstOrNull { audioFormatIDPreference.indexOfFirst { a -> a.contains(it.format_id) } >= 0 || it.container == audioContainer} ?: throw Exception() requirements.add {it: Format -> audioFormatIDPreference.contains(it.format_id)}
}catch (e: Exception){ requirements.add {it: Format -> it.container == audioContainer }
formats.first { it.format_note.contains("audio", ignoreCase = true) } 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){ }catch (e: Exception){
bestAudioFormat bestAudioFormat
} }
@ -408,7 +408,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
} }
else /*best*/ -> { else /*best*/ -> {
val requirements: MutableList<(Format) -> Boolean> = mutableListOf() 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 -> 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 -> it.format_note.contains(videoQualityPreference.split("_")[0].dropLast(1)) }
requirements.add { it: Format -> "^(${videoCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)} 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") var preferredType = sharedPreferences.getString("preferred_download_type", "video")
items.forEach { items.forEach {
preferredType = getDownloadType(Type.valueOf(preferredType!!), it!!.url).toString() 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 return list
} }
@ -656,7 +656,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
} }
fun getTotalSize(status: List<DownloadRepository.Status>) : LiveData<Int> { 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 { fun checkAllQueuedItemsAreScheduledAfterNow(items: List<Long>, inverted: Boolean, currentStartTime: Long) : Boolean {

@ -64,18 +64,20 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
deleteAll() deleteAll()
} }
} }
suspend fun parseQueries(inputQueries: List<String>){ suspend fun parseQueries(inputQueries: List<String>) : MutableList<ResultItem?>{
state = ResultsState.PROCESSING state = ResultsState.PROCESSING
if (inputQueries.size == 1){ return if (inputQueries.size == 1){
parseQuery(inputQueries[0], true) parseQuery(inputQueries[0], true)
}else { }else {
repository.itemCount.value = inputQueries.size repository.itemCount.value = inputQueries.size
loadingItems.postValue(true) loadingItems.postValue(true)
val items = mutableListOf<ResultItem?>()
inputQueries.forEach { inputQueries.forEach {
parseQuery(it, false) items.addAll(parseQuery(it, false))
} }
state = ResultsState.IDLE state = ResultsState.IDLE
loadingItems.postValue(false) 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() } runCatching { supportFragmentManager.popBackStack() }
quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false) || sharedPreferences.getString("preferred_download_type", "video") == "command") 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){ val intentData = when(action){
Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)!! Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)!!
else -> intent.dataString!! else -> intent.dataString!!
@ -132,15 +121,16 @@ class ShareActivity : BaseActivity() {
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val result = resultViewModel.getItemByURL(inputQuery) val result = withContext(Dispatchers.IO){
loadingBottomSheet.dismiss() resultViewModel.getItemByURL(inputQuery)
}
if (result == null) throw Exception()
showDownloadSheet(result) showDownloadSheet(result)
}catch (e: Exception){ }catch (e: Exception){
resultViewModel.deleteAll() resultViewModel.deleteAll()
if (quickDownload && matcher.matches()){ showDownloadSheet(downloadViewModel.createEmptyResultItem(inputQuery))
val result = downloadViewModel.createEmptyResultItem(inputQuery) if (matcher.matches()){
loadingBottomSheet.dismiss()
showDownloadSheet(result)
}else{ }else{
val res = withContext(Dispatchers.IO){ val res = withContext(Dispatchers.IO){
resultViewModel.parseQuery(inputQuery, true) resultViewModel.parseQuery(inputQuery, true)
@ -149,7 +139,6 @@ class ShareActivity : BaseActivity() {
Toast.makeText(this@ShareActivity, "No Results Found!", Toast.LENGTH_SHORT).show() Toast.makeText(this@ShareActivity, "No Results Found!", Toast.LENGTH_SHORT).show()
exit() exit()
}else{ }else{
loadingBottomSheet.dismiss()
if (res.size == 1){ if (res.size == 1){
showDownloadSheet(res[0]!!) showDownloadSheet(res[0]!!)
}else{ }else{
@ -163,12 +152,17 @@ class ShareActivity : BaseActivity() {
} }
private fun showDownloadSheet(it: ResultItem){ 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)){ 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") bottomSheet.show(supportFragmentManager, "downloadSingleSheet")
}else{ }else{
lifecycleScope.launch(Dispatchers.IO){ 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)) downloadViewModel.queueDownloads(listOf(downloadItem))
} }
this.finish() this.finish()
@ -184,7 +178,9 @@ class ShareActivity : BaseActivity() {
val downloadItems = mutableListOf<DownloadItem>() val downloadItems = mutableListOf<DownloadItem>()
lifecycleScope.launch(Dispatchers.IO){ lifecycleScope.launch(Dispatchers.IO){
it.forEach { res -> 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() i.format = downloadViewModel.getLatestCommandTemplateAsFormat()
downloadItems.add(i) 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.Context.CLIPBOARD_SERVICE
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@ -23,8 +21,6 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.forEachIndexed
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider 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.InfoUtil
import com.deniscerri.ytdlnis.util.ThemeUtil import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.devbrackets.android.exomedia.core.video.mp.NativeVideoDelegate
import com.facebook.shimmer.ShimmerFrameLayout import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
@ -61,7 +56,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.util.* import java.util.*
@ -168,7 +162,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}else if (resultViewModel.repository.itemCount.value == 1){ }else if (resultViewModel.repository.itemCount.value == 1){
if (sharedPreferences!!.getBoolean("download_card", true)){ if (sharedPreferences!!.getBoolean("download_card", true)){
if(it.size == 1 && quickLaunchSheet && parentFragmentManager.findFragmentByTag("downloadSingleSheet") == null){ 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{ }else{
@ -212,7 +209,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
if (arguments?.getString("url") != null){ if (arguments?.getString("url") != null){
val url = requireArguments().getString("url") val url = requireArguments().getString("url")
if (inputQueries == null) inputQueries = mutableListOf() if (inputQueries == null) inputQueries = mutableListOf()
searchBar?.text = url searchBar?.setText(url)
val argList = url!!.split("\n").toMutableList() val argList = url!!.split("\n").toMutableList()
argList.removeAll(listOf("", null)) argList.removeAll(listOf("", null))
inputQueries!!.addAll(argList) 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() { override fun onResume() {
@ -267,7 +252,20 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}else{ }else{
arguments?.remove("url") 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){ if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){
lifecycleScope.launch { lifecycleScope.launch {
updateSearchViewItems(searchView?.editText?.text, linkYouCopied) updateSearchViewItems(searchView?.editText?.text, linkYouCopied)
@ -412,7 +410,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
R.id.delete_results -> { R.id.delete_results -> {
resultViewModel.getTrending() resultViewModel.getTrending()
selectedObjects = ArrayList() selectedObjects = ArrayList()
searchBar!!.text = "" searchBar!!.setText("")
downloadAllFabCoordinator!!.visibility = GONE downloadAllFabCoordinator!!.visibility = GONE
downloadFabs!!.visibility = GONE downloadFabs!!.visibility = GONE
} }
@ -538,7 +536,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
if (queryList.isEmpty()) return if (queryList.isEmpty()) return
if (queryList.size == 1){ if (queryList.size == 1){
searchBar!!.text = searchView.text searchBar!!.setText(searchView.text)
} }
searchView.hide() searchView.hide()
@ -552,13 +550,18 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
Thread.sleep(300) Thread.sleep(300)
if(sharedPreferences!!.getBoolean("quick_download", false) || sharedPreferences!!.getString("preferred_download_type", "video") == "command"){ 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())){ 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)) { 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 { } else {
lifecycleScope.launch{ lifecycleScope.launch{
val downloadItem = withContext(Dispatchers.IO){ 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)) downloadViewModel.queueDownloads(listOf(downloadItem))
} }
@ -585,11 +588,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
Log.e(TAG, resultsList!![0].toString() + " " + videoURL) Log.e(TAG, resultsList!![0].toString() + " " + videoURL)
recyclerView!!.findViewWithTag<MaterialButton>("""${item?.url}##$type""") recyclerView!!.findViewWithTag<MaterialButton>("""${item?.url}##$type""")
if (sharedPreferences!!.getBoolean("download_card", true)) { if (sharedPreferences!!.getBoolean("download_card", true)) {
showSingleDownloadSheet(item!!, type!!, false) showSingleDownloadSheet(item!!, type!!)
} else { } else {
lifecycleScope.launch{ lifecycleScope.launch{
val downloadItem = withContext(Dispatchers.IO){ val downloadItem = withContext(Dispatchers.IO){
downloadViewModel.createDownloadItemFromResult(item!!, type!!) downloadViewModel.createDownloadItemFromResult(
result = item!!,
givenType = type!!)
} }
downloadViewModel.queueDownloads(listOf(downloadItem)) downloadViewModel.queueDownloads(listOf(downloadItem))
} }
@ -599,11 +604,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
override fun onLongButtonClick(videoURL: String, type: DownloadViewModel.Type?) { override fun onLongButtonClick(videoURL: String, type: DownloadViewModel.Type?) {
Log.e(TAG, type.toString() + " " + videoURL) Log.e(TAG, type.toString() + " " + videoURL)
val item = resultsList!!.find { it?.url == 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){ private fun showSingleDownloadSheet(
val bottomSheet = DownloadBottomSheetDialog(resultItem, downloadViewModel.getDownloadType(type, resultItem.url), null, quickDownload) resultItem: ResultItem,
type: DownloadViewModel.Type
){
val bottomSheet = DownloadBottomSheetDialog(resultItem, downloadViewModel.getDownloadType(type, resultItem.url))
bottomSheet.show(parentFragmentManager, "downloadSingleSheet") bottomSheet.show(parentFragmentManager, "downloadSingleSheet")
} }
@ -696,7 +704,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
R.id.download -> { R.id.download -> {
lifecycleScope.launch { lifecycleScope.launch {
if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects!!.size == 1) { 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{ }else{
val downloadList = withContext(Dispatchers.IO){ val downloadList = withContext(Dispatchers.IO){
downloadViewModel.turnResultItemsToDownloadItems(selectedObjects!!) downloadViewModel.turnResultItemsToDownloadItems(selectedObjects!!)
@ -704,7 +715,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
if (sharedPreferences!!.getBoolean("download_card", true)) { if (sharedPreferences!!.getBoolean("download_card", true)) {
if (selectedObjects!!.size == 1){ 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{ }else{
val bottomSheet = DownloadMultipleBottomSheetDialog(downloadList.toMutableList()) val bottomSheet = DownloadMultipleBottomSheetDialog(downloadList.toMutableList())
bottomSheet.show(parentFragmentManager, "downloadMultipleSheet") 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) @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -143,9 +144,14 @@ class AddExtraCommandsDialog(private val item: DownloadItem?, private val callba
shortcuts.setOnClickListener { shortcuts.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
if (shortcutCount > 0){ if (shortcutCount > 0){
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel) {sh -> UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel,
text.text.insert(text.selectionStart, " $sh ") 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.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -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
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
//TODO MAKE CONFIGURE LIKE NORMAL SHEET
class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, private val downloadItem: DownloadItem, private val listener: OnDownloadItemUpdateListener) : BottomSheetDialogFragment() { class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private val currentDownloadItem: DownloadItem, private val listener: OnDownloadItemUpdateListener) : BottomSheetDialogFragment() {
private lateinit var tabLayout: TabLayout private lateinit var tabLayout: TabLayout
private lateinit var viewPager2: ViewPager2 private lateinit var viewPager2: ViewPager2
private lateinit var fragmentAdapter : DownloadFragmentAdapter private lateinit var fragmentAdapter : DownloadFragmentAdapter
@ -80,17 +76,12 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri
overScrollMode = View.OVER_SCROLL_NEVER overScrollMode = View.OVER_SCROLL_NEVER
} }
val fragments = mutableListOf<Fragment>(
DownloadAudioFragment(resultItem, downloadItem),
DownloadVideoFragment(resultItem, downloadItem)
)
var commandTemplateNr = 0 var commandTemplateNr = 0
lifecycleScope.launch{ lifecycleScope.launch{
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
commandTemplateNr = commandTemplateDao.getTotalNumber() commandTemplateNr = commandTemplateDao.getTotalNumber()
if(commandTemplateNr > 0){ if(commandTemplateNr <= 0){
fragments.add(DownloadCommandFragment(resultItem, downloadItem))
}else{
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.alpha = 0.3f (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 //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){ if (isAudioOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
@ -107,15 +98,15 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri
val fragmentManager = parentFragmentManager val fragmentManager = parentFragmentManager
fragmentAdapter = DownloadFragmentAdapter( fragmentAdapter = DownloadFragmentAdapter(
resultItem,
fragmentManager, fragmentManager,
lifecycle, lifecycle,
fragments result,
currentDownloadItem
) )
viewPager2.adapter = fragmentAdapter viewPager2.adapter = fragmentAdapter
viewPager2.isSaveFromParentEnabled = false viewPager2.isSaveFromParentEnabled = false
when(downloadItem.type) { when(currentDownloadItem.type) {
Type.audio -> { Type.audio -> {
tabLayout.selectTab(tabLayout.getTabAt(0)) tabLayout.selectTab(tabLayout.getTabAt(0))
viewPager2.setCurrentItem(0, false) 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) val ok = view.findViewById<Button>(R.id.bottom_sheet_ok)
ok!!.setOnClickListener { ok!!.setOnClickListener {
val item = getDownloadItem() val item = getDownloadItem()
onDownloadItemUpdateListener.onDownloadItemUpdate(resultItem.id, item) onDownloadItemUpdateListener.onDownloadItemUpdate(result.id, item)
dismiss() dismiss()
} }
val link = view.findViewById<Button>(R.id.bottom_sheet_link) val link = view.findViewById<Button>(R.id.bottom_sheet_link)
link.text = resultItem.url link.text = result.url
link.setOnClickListener{ link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), resultItem.url, null) UiUtil.openLinkIntent(requireContext(), result.url, null)
} }
link.setOnLongClickListener{ link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), resultItem.url, null) UiUtil.copyLinkToClipBoard(requireContext(), result.url, null)
true true
} }
@ -211,10 +202,10 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri
if (viewPager2.currentItem == 1) 0 else 1 if (viewPager2.currentItem == 1) 0 else 1
) )
resultItem.title = prevDownloadItem.title result.title = prevDownloadItem.title
resultItem.author = prevDownloadItem.author result.author = prevDownloadItem.author
downloadItem.title = prevDownloadItem.title currentDownloadItem.title = prevDownloadItem.title
downloadItem.author = prevDownloadItem.author currentDownloadItem.author = prevDownloadItem.author
when(viewPager2.currentItem){ when(viewPager2.currentItem){
0 -> { 0 -> {

@ -12,7 +12,8 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.TextView 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.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope 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
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
@ -37,7 +37,7 @@ import kotlinx.coroutines.withContext
import java.io.File 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 fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
@ -69,18 +69,20 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
lifecycleScope.launch { lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO) { downloadItem = withContext(Dispatchers.IO) {
if (currentDownloadItem != null){ if (currentDownloadItem != null){
//object cloning
val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java) val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java) Gson().fromJson(string, DownloadItem::class.java)
}else{ }else{
downloadViewModel.createDownloadItemFromResult(resultItem, Type.audio) downloadViewModel.createDownloadItemFromResult(resultItem, url, Type.audio)
} }
} }
val sharedPreferences = val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
PreferenceManager.getDefaultSharedPreferences(requireContext())
try { try {
title = view.findViewById(R.id.title_textinput) 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 { title.editText!!.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(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() 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 = 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 { author.editText!!.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(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() 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 = view.findViewById(R.id.outputPath)
saveDir.editText!!.setText( saveDir.editText!!.setText(
FileUtil.formatPath(downloadItem.downloadPath) 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_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_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) freeSpace = view.findViewById(R.id.freespace)
val free = FileUtil.convertFileSize( val free = FileUtil.convertFileSize(
@ -119,7 +160,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
var formats = mutableListOf<Format>() var formats = mutableListOf<Format>()
if (currentDownloadItem == null) { 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{ }else{
//if its updating a present downloaditem and its the wrong category //if its updating a present downloaditem and its the wrong category
if (currentDownloadItem!!.type != Type.audio){ 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) UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, null)
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
resultItem.formats.removeAll(formats.toSet()) resultItem?.formats?.removeAll(formats.toSet())
resultItem.formats.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) }) resultItem?.formats?.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) })
resultViewModel.update(resultItem) if (resultItem != null){
resultViewModel.update(resultItem!!)
}
} }
} }
formats = allFormats.first().filter { !genericAudioFormats.contains(it) }.toMutableList() formats = allFormats.first().filter { !genericAudioFormats.contains(it) }.toMutableList()
@ -209,7 +252,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
}, },
sponsorBlockItemsSet = { values, checkedItems -> sponsorBlockItemsSet = { values, checkedItems ->
downloadItem.audioPreferences.sponsorBlockFilters.clear() downloadItem.audioPreferences.sponsorBlockFilters.clear()
for (i in 0 until checkedItems.size) { for (i in checkedItems.indices) {
if (checkedItems[i]) { if (checkedItems[i]) {
downloadItem.audioPreferences.sponsorBlockFilters.add(values[i]) downloadItem.audioPreferences.sponsorBlockFilters.add(values[i])
} }
@ -217,7 +260,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
}, },
cutClicked = {cutVideoListener -> cutClicked = {cutVideoListener ->
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){ 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") bottomSheet.show(parentFragmentManager, "cutVideoSheet")
} }
}, },
@ -242,29 +285,15 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
downloadItem.title = t downloadItem.title = t
downloadItem.author = a downloadItem.author = a
title.editText?.setText(t) title.editText?.setText(t)
title.endIconDrawable = null
author.editText?.setText(a) author.editText?.setText(a)
title.endIconDrawable = null
} }
private var audioPathResultLauncher = registerForActivityResult( override fun updateUI(res: ResultItem?){
ActivityResultContracts.StartActivityForResult() resultItem = res
) { result -> val state = Bundle()
if (result.resultCode == Activity.RESULT_OK) { state.putBoolean("updated", true)
result.data?.data?.let { onViewCreated(requireView(),savedInstanceState = state)
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
}
} }
} }

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.text.format.DateFormat import android.text.format.DateFormat
@ -15,17 +16,18 @@ import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.models.DownloadItem 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.models.ResultItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel 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.receiver.ShareActivity
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* 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 tabLayout: TabLayout
private lateinit var viewPager2: ViewPager2 private lateinit var viewPager2: ViewPager2
private lateinit var fragmentAdapter : DownloadFragmentAdapter 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 behavior: BottomSheetBehavior<View>
private lateinit var commandTemplateDao : CommandTemplateDao private lateinit var commandTemplateDao : CommandTemplateDao
private lateinit var infoUtil: InfoUtil 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao
infoUtil = InfoUtil(requireContext()) infoUtil = InfoUtil(requireContext())
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi", "InflateParams")
override fun setupDialog(dialog: Dialog, style: Int) { override fun setupDialog(dialog: Dialog, style: Int) {
super.setupDialog(dialog, style) 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.setContentView(view)
dialog.setOnShowListener { dialog.setOnShowListener {
@ -89,18 +93,12 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
overScrollMode = View.OVER_SCROLL_NEVER overScrollMode = View.OVER_SCROLL_NEVER
} }
downloadAudioFragment = DownloadAudioFragment(resultItem, downloadItem)
downloadVideoFragment = DownloadVideoFragment(resultItem, downloadItem)
val fragments = mutableListOf<Fragment>(downloadAudioFragment, downloadVideoFragment)
var commandTemplateNr = 0 var commandTemplateNr = 0
lifecycleScope.launch{ lifecycleScope.launch{
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
commandTemplateNr = commandTemplateDao.getTotalNumber() commandTemplateNr = commandTemplateDao.getTotalNumber()
if (!Patterns.WEB_URL.matcher(resultItem.url).matches()) commandTemplateNr++ if (!Patterns.WEB_URL.matcher(result.url).matches()) commandTemplateNr++
if(commandTemplateNr > 0){ if(commandTemplateNr <= 0){
downloadCommandFragment = DownloadCommandFragment(resultItem, downloadItem)
fragments.add(downloadCommandFragment)
}else{
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.alpha = 0.3f (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 //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){ if (isAudioOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
} }
//check if the item is coming from a text file //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){ if (isCommandOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(0)?.isClickable = false (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(0)?.isClickable = false
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(0)?.alpha = 0.3f (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 val fragmentManager = parentFragmentManager
fragmentAdapter = DownloadFragmentAdapter( fragmentAdapter = DownloadFragmentAdapter(
resultItem,
fragmentManager, fragmentManager,
lifecycle, lifecycle,
fragments result,
currentDownloadItem
) )
viewPager2.adapter = fragmentAdapter 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 link = view.findViewById<Button>(R.id.bottom_sheet_link)
val updateItem = view.findViewById<Button>(R.id.update_item) val updateItem = view.findViewById<Button>(R.id.update_item)
if (Patterns.WEB_URL.matcher(resultItem.url).matches()){ if (Patterns.WEB_URL.matcher(result.url).matches()){
link.text = resultItem.url link.text = result.url
link.setOnClickListener{ link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), resultItem.url, null) UiUtil.openLinkIntent(requireContext(), result.url, null)
} }
link.setOnLongClickListener{ link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), resultItem.url, null) UiUtil.copyLinkToClipBoard(requireContext(), result.url, null)
true true
} }
if (quickDownload) { //if auto-update after the card is open is off
(updateItem.parent as LinearLayout).visibility = View.VISIBLE if (result.title.isEmpty() && currentDownloadItem == null) {
updateItem.setOnClickListener { if(sharedPreferences.getBoolean("quick_download", false)) {
if (activity is ShareActivity) { (updateItem.parent as LinearLayout).visibility = View.VISIBLE
dismiss() updateItem.setOnClickListener {
val intent = Intent(context, ShareActivity::class.java) (updateItem.parent as LinearLayout).visibility = View.GONE
intent.action = Intent.ACTION_SEND initUpdateData(view)
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
)
} }
} }
}else{ }else{
@ -278,6 +264,12 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
link.visibility = View.GONE link.visibility = View.GONE
updateItem.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{ private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{
return when(selectedTabPosition){ return when(selectedTabPosition){
@ -300,11 +292,7 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
val prevDownloadItem = getDownloadItem( val prevDownloadItem = getDownloadItem(
if (viewPager2.currentItem == 1) 0 else 1 if (viewPager2.currentItem == 1) 0 else 1
) )
fragmentAdapter.setTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
resultItem.title = prevDownloadItem.title
resultItem.author = prevDownloadItem.author
downloadItem?.title = prevDownloadItem.title
downloadItem?.author = prevDownloadItem.author
when(viewPager2.currentItem){ when(viewPager2.currentItem){
0 -> { 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) { override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog) super.onCancel(dialog)
cleanUp() cleanUp()
@ -332,13 +389,11 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
private fun cleanUp(){ private fun cleanUp(){
kotlin.runCatching { kotlin.runCatching {
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("downloadSingleSheet")!!).commit() repeat(parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.backStackEntryCount ?: 0){
parentFragmentManager.beginTransaction().remove(downloadVideoFragment).commit() parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.popBackStack()
parentFragmentManager.beginTransaction().remove(downloadVideoFragment).commit()
if (this::downloadCommandFragment.isInitialized){
parentFragmentManager.beginTransaction().remove(downloadCommandFragment).commit()
} }
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() (activity as ShareActivity).finish()
} }
} }

@ -41,7 +41,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File 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 fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel 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) val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java) Gson().fromJson(string, DownloadItem::class.java)
}else{ }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!!.setText("")
} }
} }
chosenCommandView.editText!!.enableScrollText()
val commandTemplateCard = view.findViewById<MaterialCardView>(R.id.command_card) val commandTemplateCard = view.findViewById<MaterialCardView>(R.id.command_card)
runCatching { runCatching {
@ -230,10 +229,19 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
} }
}, },
shortcutClicked = { shortcutClicked = {
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel) { sh -> UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel,
chosenCommandView.editText!!.setText("${chosenCommandView.editText!!.text} $sh") itemSelected = {
downloadItem.format.format_note += " $sh" 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.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
class DownloadFragmentAdapter ( class DownloadFragmentAdapter (
private val resultItem: ResultItem,
fragmentManager : FragmentManager, fragmentManager : FragmentManager,
lifecycle : Lifecycle, lifecycle : Lifecycle,
private val fragments: List<Fragment> private var result: ResultItem,
private var downloadItem: DownloadItem?
) : FragmentStateAdapter(fragmentManager, lifecycle) { ) : 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 { override fun getItemCount(): Int {
return fragments.size return 3
} }
override fun createFragment(position: Int): Fragment { 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.os.Bundle
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.view.inputmethod.InputMethodManager
import android.widget.Button import android.widget.Button
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.get import androidx.core.view.get
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -41,7 +34,6 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter 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.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.repository.DownloadRepository 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.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.reflect.Type
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -628,7 +618,6 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
} }
override fun onClick(p0: View?) { override fun onClick(p0: View?) {
TODO("Not yet implemented")
} }
override fun onDownloadItemUpdate(resultItemID: Long, item: DownloadItem) { override fun onDownloadItemUpdate(resultItemID: Long, item: DownloadItem) {

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

@ -8,6 +8,7 @@ import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.* import android.widget.*
import androidx.compose.runtime.invalidateGroupsWithKey
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -212,24 +213,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
selectedVideo = selectedVideo =
chosenFormats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.maxByOrNull { it.filesize }!! chosenFormats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.maxByOrNull { it.filesize }!!
} }
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) })
}
dismiss() dismiss()
} }
@ -237,9 +221,30 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
refreshBtn.performClick() 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(){ private fun addFormatsToView(){
//sort //sort
val finalFormats: List<Format> = when(sortBy){ var finalFormats: List<Format> = when(sortBy){
FormatSorting.container -> chosenFormats.groupBy { it.container }.flatMap { it.value } FormatSorting.container -> chosenFormats.groupBy { it.container }.flatMap { it.value }
FormatSorting.id -> chosenFormats.sortedBy { it.format_id } FormatSorting.id -> chosenFormats.sortedBy { it.format_id }
FormatSorting.codec -> { 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){ for (i in 0.. finalFormats.lastIndex){
val format = finalFormats[i] val format = finalFormats[i]
val formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null) 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) val clickedCard = (clickedformat as MaterialCardView)
if (format.vcodec.isNotBlank() && format.vcodec != "none") { if (format.vcodec.isNotBlank() && format.vcodec != "none") {
if (clickedCard.isChecked) { if (clickedCard.isChecked) {
listener.onFormatClick(formats, listOf(FormatTuple(format, null))) returnFormats()
dismiss() dismiss()
} }
videoFormatList.forEach { (it as MaterialCardView).isChecked = false } 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.view.Window
import android.widget.* import android.widget.*
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map import androidx.lifecycle.map
@ -87,7 +88,7 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity().applicationContext) infoUtil = InfoUtil(requireActivity())
notificationUtil = NotificationUtil(requireActivity().applicationContext) notificationUtil = NotificationUtil(requireActivity().applicationContext)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
@ -229,15 +230,14 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
downloadThumb.setOnClickListener { downloadThumb.setOnClickListener {
downloadManager.enqueue( downloadManager.enqueue(
DownloadManager.Request(Uri.parse(item.thumb)) DownloadManager.Request(item.thumb.toUri())
.setAllowedNetworkTypes( .setAllowedNetworkTypes(
DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_WIFI or
DownloadManager.Request.NETWORK_MOBILE DownloadManager.Request.NETWORK_MOBILE
) )
.setAllowedOverRoaming(true) .setAllowedOverRoaming(true)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setTitle(requireContext().getString(R.string.app_name)) .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/" + item.title + ".jpg"))
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "YTDLnis"))
} }
title.text = item.title title.text = item.title
@ -293,12 +293,14 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
private fun onButtonClick(type: DownloadViewModel.Type){ private fun onButtonClick(type: DownloadViewModel.Type){
this.dismiss() this.dismiss()
if (sharedPreferences.getBoolean("download_card", true)) { if (sharedPreferences.getBoolean("download_card", true)) {
val bottomSheet = DownloadBottomSheetDialog(item, type, null, false) val bottomSheet = DownloadBottomSheetDialog(item, type)
bottomSheet.show(parentFragmentManager, "downloadSingleSheet") bottomSheet.show(parentFragmentManager, "downloadSingleSheet")
} else { } else {
lifecycleScope.launch{ lifecycleScope.launch{
val downloadItem = withContext(Dispatchers.IO){ val downloadItem = withContext(Dispatchers.IO){
downloadViewModel.createDownloadItemFromResult(item, type) downloadViewModel.createDownloadItemFromResult(
result = item,
givenType = type)
} }
downloadViewModel.queueDownloads(listOf(downloadItem)) 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) } val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) }
if (checkedResultItems.size == 1){ if (checkedResultItems.size == 1){
val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url) val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url)
val bottomSheet = DownloadBottomSheetDialog(resultItem, type, null, false) val bottomSheet = DownloadBottomSheetDialog(resultItem, type)
bottomSheet.show(parentFragmentManager, "downloadSingleSheet") bottomSheet.show(parentFragmentManager, "downloadSingleSheet")
}else{ }else{
val downloadItems = mutableListOf<DownloadItem>() val downloadItems = mutableListOf<DownloadItem>()
checkedResultItems.forEach { c -> checkedResultItems.forEach { c ->
c!!.id = 0 c!!.id = 0
val i = downloadViewModel.createDownloadItemFromResult(c,type) val i = downloadViewModel.createDownloadItemFromResult(
result = c, givenType = type)
if (type == DownloadViewModel.Type.command){ if (type == DownloadViewModel.Type.command){
i.format = downloadViewModel.getLatestCommandTemplateAsFormat() i.format = downloadViewModel.getLatestCommandTemplateAsFormat()
} }
@ -207,7 +208,7 @@ class SelectPlaylistItemsDialog(private val items: List<ResultItem?>, private va
} }
toolbar.setNavigationOnClickListener { toolbar.setNavigationOnClickListener {
requireActivity().finishAffinity() dismiss()
} }
} }
@ -224,9 +225,9 @@ class SelectPlaylistItemsDialog(private val items: List<ResultItem?>, private va
private fun cleanup(){ private fun cleanup(){
kotlin.runCatching { kotlin.runCatching {
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("downloadPlaylistSheet")!!).commit() 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") workManager.cancelAllWorkByTag("download")
pauseResume.isEnabled = false pauseResume.isEnabled = false
// pause active // pause queued
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads() downloadViewModel.getQueued()
}.forEach { }.forEach {
cancelItem(it.id.toInt()) it.status = DownloadRepository.Status.QueuedPaused.toString()
it.status = DownloadRepository.Status.Paused.toString()
downloadViewModel.updateDownload(it) downloadViewModel.updateDownload(it)
} }
// pause queued // pause active
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.getQueued() downloadViewModel.getActiveDownloads()
}.forEach { }.forEach {
it.status = DownloadRepository.Status.QueuedPaused.toString() cancelItem(it.id.toInt())
it.status = DownloadRepository.Status.Paused.toString()
downloadViewModel.updateDownload(it) downloadViewModel.updateDownload(it)
} }
activeDownloads.notifyDataSetChanged() activeDownloads.notifyDataSetChanged()
pauseResume.isEnabled = true pauseResume.isEnabled = true
} }

@ -127,7 +127,11 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
} }
}, },
longClickDownloadButton = { 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") sheet.show(parentFragmentManager, "downloadSingleSheet")
}, },
scheduleButtonClick = {} scheduleButtonClick = {}

@ -99,6 +99,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
downloadViewModel.getItemByID(itemID) downloadViewModel.getItemByID(itemID)
} }
if (item.logID != null) { if (item.logID != null) {
actionMode?.finish()
val bundle = Bundle() val bundle = Bundle()
bundle.putLong("logID", item.logID!!) bundle.putLong("logID", item.logID!!)
findNavController().navigate( findNavController().navigate(
@ -129,7 +130,11 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
} }
}, },
longClickDownloadButton = { 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") sheet.show(parentFragmentManager, "downloadSingleSheet")
}, },
scheduleButtonClick = {} scheduleButtonClick = {}

@ -527,7 +527,9 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
redownload.setOnLongClickListener { redownload.setOnLongClickListener {
bottomSheet!!.cancel() 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") sheet.show(parentFragmentManager, "downloadSingleSheet")
true true
} }
@ -669,7 +671,9 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
ItemTouchHelper.RIGHT -> { ItemTouchHelper.RIGHT -> {
val item = historyList!![position]!! val item = historyList!![position]!!
historyAdapter!!.notifyItemChanged(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") sheet.show(parentFragmentManager, "downloadSingleSheet")
} }
} }

@ -150,7 +150,11 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
} }
}, },
longClickDownloadButton = { 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") sheet.show(parentFragmentManager, "downloadSingleSheet")
}, },
scheduleButtonClick = {downloadItem -> scheduleButtonClick = {downloadItem ->

@ -127,7 +127,11 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
} }
}, },
longClickDownloadButton = { it: DownloadItem -> 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") sheet.show(parentFragmentManager, "downloadSingleSheet")
}, },
scheduleButtonClick = {} scheduleButtonClick = {}

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

@ -187,7 +187,6 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
} }
override fun onSelected(cookie: CookieItem) { override fun onSelected(cookie: CookieItem) {
TODO("Not yet implemented")
} }
override fun onDelete(cookie: CookieItem) { 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.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.more.settings.SettingsActivity import com.deniscerri.ytdlnis.ui.more.settings.SettingsActivity
import com.deniscerri.ytdlnis.ui.more.terminal.TerminalActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.system.exitProcess import kotlin.system.exitProcess
class MoreFragment : Fragment() { class MoreFragment : Fragment() {

@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.ui.more.settings
import android.os.Bundle import android.os.Bundle
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
@ -10,9 +11,23 @@ class ProcessingSettingsFragment : BaseSettingsFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.processing_preferences, rootKey) 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 preferredFormatID : EditTextPreference? = findPreference("format_id")
val preferredFormatIDAudio : EditTextPreference? = findPreference("format_id_audio") 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?.title = "${getString(R.string.preferred_format_id)} [${getString(R.string.video)}]"
preferredFormatID?.dialogTitle = "${getString(R.string.file_name_template)} [${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.ActionBar.LayoutParams
import android.app.Activity import android.app.Activity
import android.content.ClipboardManager 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.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PersistableBundle import android.os.PersistableBundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -23,22 +23,27 @@ import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts 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.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.workDataOf
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.TerminalItem
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel 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.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableTextHighlight 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.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -46,13 +51,13 @@ import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
import kotlin.properties.Delegates import kotlin.properties.Delegates
class TerminalActivity : BaseActivity() { class TerminalFragment : Fragment() {
private var topAppBar: MaterialToolbar? = null private var topAppBar: MaterialToolbar? = null
private lateinit var notificationUtil: NotificationUtil private lateinit var notificationUtil: NotificationUtil
private lateinit var terminalViewModel: TerminalViewModel
private var output: TextView? = null private var output: TextView? = null
private var input: EditText? = null private var input: EditText? = null
private var fab: ExtendedFloatingActionButton? = null private var fab: ExtendedFloatingActionButton? = null
@ -60,35 +65,68 @@ class TerminalActivity : BaseActivity() {
private lateinit var bottomAppBar: BottomAppBar private lateinit var bottomAppBar: BottomAppBar
private lateinit var commandTemplateViewModel: CommandTemplateViewModel private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private var downloadID by Delegates.notNull<Int>() private var downloadID by Delegates.notNull<Long>()
private lateinit var downloadFile : File
private lateinit var imm : InputMethodManager private lateinit var imm : InputMethodManager
private lateinit var metrics: DisplayMetrics private lateinit var metrics: DisplayMetrics
var context: Context? = null
public override fun onCreate(savedInstanceState: Bundle?) { override fun onCreateView(
super.onCreate(savedInstanceState) inflater: LayoutInflater,
setContentView(R.layout.activity_terminal) 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) input = view.findViewById(R.id.command_edittext)
downloadFile = File(cacheDir.absolutePath + "/$downloadID.txt")
if (! downloadFile.exists()) downloadFile.createNewFile()
imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
context = baseContext if (arguments?.getLong("id") != null){
scrollView = findViewById(R.id.custom_command_scrollview) downloadID = requireArguments().getLong("id")
topAppBar = findViewById(R.id.custom_command_toolbar) if(downloadID != 0L){
topAppBar!!.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } 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] commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
metrics = DisplayMetrics() 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 templateCount = 0
var shortcutCount = 0 var shortcutCount = 0
lifecycleScope.launch { lifecycleScope.launch {
templateCount = withContext(Dispatchers.IO){ templateCount = withContext(Dispatchers.IO){
commandTemplateViewModel.getTotalNumber() commandTemplateViewModel.getTotalNumber()
@ -112,10 +150,10 @@ class TerminalActivity : BaseActivity() {
when(it.itemId){ when(it.itemId){
R.id.command_templates -> { R.id.command_templates -> {
if (templateCount == 0){ 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{ }else{
lifecycleScope.launch { lifecycleScope.launch {
UiUtil.showCommandTemplates(this@TerminalActivity, commandTemplateViewModel){ templates -> UiUtil.showCommandTemplates(requireActivity(), commandTemplateViewModel){ templates ->
templates.forEach {c -> templates.forEach {c ->
input!!.text.insert(input!!.selectionStart, c.content + " ") input!!.text.insert(input!!.selectionStart, c.content + " ")
} }
@ -130,9 +168,14 @@ class TerminalActivity : BaseActivity() {
R.id.shortcuts -> { R.id.shortcuts -> {
lifecycleScope.launch { lifecycleScope.launch {
if (shortcutCount > 0){ if (shortcutCount > 0){
UiUtil.showShortcuts(this@TerminalActivity, commandTemplateViewModel){sh -> UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel,
input!!.text.insert(input!!.selectionStart, "$sh ") 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 true
} }
output = findViewById(R.id.custom_command_output) output = view.findViewById(R.id.custom_command_output)
output!!.setTextIsSelectable(true) output!!.setTextIsSelectable(true)
output!!.layoutParams!!.width = LayoutParams.WRAP_CONTENT output!!.layoutParams!!.width = LayoutParams.WRAP_CONTENT
input = findViewById(R.id.command_edittext)
input!!.requestFocus() input!!.requestFocus()
fab = findViewById(R.id.command_fab) fab = view.findViewById(R.id.command_fab)
fab!!.setOnClickListener { fab!!.setOnClickListener {
if (fab!!.text == getString(R.string.run_command)){ if (fab!!.text == getString(R.string.run_command)){
input!!.visibility = View.GONE input!!.visibility = View.GONE
output!!.text = "${output!!.text}\n~ $ ${input!!.text}\n" output!!.text = "${output!!.text}\n~ $ ${input!!.text}\n"
showCancelFab() showCancelFab()
imm.hideSoftInputFromWindow(input?.windowToken, 0) imm.hideSoftInputFromWindow(input?.windowToken, 0)
startDownload( lifecycleScope.launch {
input!!.text.toString() val command = input!!.text.toString().replace("yt-dlp", "")
) downloadID = withContext(Dispatchers.IO){
terminalViewModel.insert(TerminalItem(command = command))
}
terminalViewModel.startTerminalDownloadWorker(TerminalItem(downloadID, command))
runWorkerListener()
}
}else { }else {
cancelDownload() terminalViewModel.cancelTerminalDownload(downloadID)
input!!.visibility = View.VISIBLE input!!.visibility = View.VISIBLE
hideCancelFab() hideCancelFab()
} }
} }
notificationUtil = NotificationUtil(this) notificationUtil = NotificationUtil(requireContext())
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) {}
}
}
}
}
initMenu() initMenu()
input?.enableTextHighlight() input?.enableTextHighlight()
output?.enableTextHighlight() output?.enableTextHighlight()
input?.setText(savedInstanceState?.getString("input") ?: input?.text) input?.append(bundle?.getString("input") ?: "")
input!!.requestFocus() input!!.requestFocus()
input!!.setSelection(input!!.text.length) input!!.setSelection(input!!.text.length)
output?.text = savedInstanceState?.getString("output") ?: output?.text output?.text = bundle?.getString("output") ?: output?.text
if (savedInstanceState?.getBoolean("run") == true){ if (bundle?.getBoolean("run") == true){
showCancelFab() showCancelFab()
} }
} runWorkerListener()
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)
} }
private fun initMenu() { 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 -> topAppBar?.setOnMenuItemClickListener { m: MenuItem ->
when(m.itemId){ when(m.itemId){
R.id.wrap -> { R.id.wrap -> {
var scrollView = findViewById<HorizontalScrollView>(R.id.horizontalscroll_output) var scrollView = requireView().findViewById<HorizontalScrollView>(R.id.horizontalscroll_output)
if(scrollView != null){ if(scrollView != null){
val parent = (scrollView.parent as ViewGroup) val parent = (scrollView.parent as ViewGroup)
scrollView.removeAllViews() scrollView.removeAllViews()
@ -232,7 +248,7 @@ class TerminalActivity : BaseActivity() {
}else{ }else{
val parent = output?.parent as ViewGroup val parent = output?.parent as ViewGroup
parent.removeView(output) parent.removeView(output)
scrollView = HorizontalScrollView(this) scrollView = HorizontalScrollView(requireContext())
scrollView.layoutParams = LinearLayout.LayoutParams( scrollView.layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT ViewGroup.LayoutParams.MATCH_PARENT
@ -244,7 +260,7 @@ class TerminalActivity : BaseActivity() {
} }
R.id.export_clipboard -> { R.id.export_clipboard -> {
lifecycleScope.launch(Dispatchers.IO){ 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) clipboard.setText(output?.text)
} }
} }
@ -252,39 +268,6 @@ class TerminalActivity : BaseActivity() {
true 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() { private fun hideCancelFab() {
fab!!.text = getString(R.string.run_command) fab!!.text = getString(R.string.run_command)
fab!!.setIconResource(R.drawable.ic_baseline_keyboard_arrow_right_24) fab!!.setIconResource(R.drawable.ic_baseline_keyboard_arrow_right_24)
@ -299,7 +282,7 @@ class TerminalActivity : BaseActivity() {
) { result -> ) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { result.data?.data?.let {
contentResolver?.takePersistableUriPermission( requireActivity().contentResolver?.takePersistableUriPermission(
it, it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION
@ -309,34 +292,43 @@ class TerminalActivity : BaseActivity() {
} }
} }
private fun startDownload(command: String?) { private fun runWorkerListener(){
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()
WorkManager.getInstance(this).beginUniqueWork( WorkManager.getInstance(requireContext())
downloadID.toString(), .getWorkInfosByTagLiveData(downloadID.toString())
ExistingWorkPolicy.KEEP, .observe(viewLifecycleOwner){ list ->
workRequest list.forEach {work ->
).enqueue() 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 package com.deniscerri.ytdlnis.util
import android.app.Activity
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent
import android.database.Cursor import android.database.Cursor
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
@ -10,15 +12,21 @@ import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import android.view.View
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile 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.FileCallback
import com.anggrayudi.storage.callback.FolderCallback import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.copyFolderTo import com.anggrayudi.storage.file.copyFolderTo
import com.anggrayudi.storage.file.getAbsolutePath import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.moveFileTo import com.anggrayudi.storage.file.moveFileTo
import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -250,7 +258,11 @@ object FileUtil {
fun scanMedia(files: List<String>, context: Context) : List<String> { fun scanMedia(files: List<String>, context: Context) : List<String> {
try { try {
val paths = files.sortedByDescending { File(it).length() } 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 return paths
}catch (e: Exception){ }catch (e: Exception){
e.printStackTrace() e.printStackTrace()
@ -296,4 +308,20 @@ object FileUtil {
val symbols = DecimalFormatSymbols(Locale.US) val symbols = DecimalFormatSymbols(Locale.US)
return "${DecimalFormat("#,##0.#", symbols).format(s / 1024.0.pow(digitGroups.toDouble()))} ${units[digitGroups]}" 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 package com.deniscerri.ytdlnis.util
import android.app.Activity
import android.app.DownloadManager
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources 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.os.Looper
import android.text.Html import android.text.Html
import android.util.Log import android.util.Log
import android.util.Patterns import android.util.Patterns
import android.util.TypedValue
import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.navigation.findNavController
import androidx.preference.AndroidResources import androidx.preference.AndroidResources
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.ChapterItem import com.deniscerri.ytdlnis.database.models.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format 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.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel 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.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest 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.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
@ -48,20 +72,14 @@ class InfoUtil(private val context: Context) {
fun search(query: String): ArrayList<ResultItem?> { fun search(query: String): ArrayList<ResultItem?> {
return when(sharedPreferences.getString("search_engine", "ytsearch")){ return runCatching {
"ytsearch" -> try{ when(sharedPreferences.getString("search_engine", "ytsearch")){
searchFromPiped(query) "ytsearch" -> searchFromPiped(query)
}catch (e: Exception){ "ytsearchmusic" -> searchFromPipedMusic(query)
getFromYTDL(query) else -> throw Exception()
}
"ytsearchmusic" -> try{
searchFromPipedMusic(query)
}catch (e: Exception){
getFromYTDL(query)
} }
}.getOrElse {
else -> getFromYTDL(query) getFromYTDL(query)
} }
} }
@ -576,11 +594,9 @@ class InfoUtil(private val context: Context) {
} }
} }
val proxy = sharedPreferences.getString("proxy", "") val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){ if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy) request.addOption("--proxy", proxy)
} }
val youtubeDLResponse = YoutubeDL.getInstance().execute(request) val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
@ -622,7 +638,7 @@ class InfoUtil(private val context: Context) {
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url") 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? = "" var playlistTitle: String? = ""
if (jsonObject.has("playlist_title")) playlistTitle = jsonObject.getString("playlist_title") if (jsonObject.has("playlist_title")) playlistTitle = jsonObject.getString("playlist_title")
if(playlistTitle.equals(query)) playlistTitle = "" if(playlistTitle.equals(query)) playlistTitle = ""
@ -673,15 +689,17 @@ class InfoUtil(private val context: Context) {
website, website,
playlistTitle!!, playlistTitle!!,
formats, formats,
urls, urls,
chapters chapters
) )
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
Looper.prepare().run { val intent = Intent(context, ErrorDialogActivity::class.java)
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() 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() e.printStackTrace()
} }
return items return items
@ -975,21 +993,25 @@ class InfoUtil(private val context: Context) {
if (aria2) { if (aria2) {
request.addOption("--downloader", "libaria2c.so") request.addOption("--downloader", "libaria2c.so")
request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"") 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 retries = sharedPreferences.getString("--retries", "")!!
val fragmentRetries = sharedPreferences.getString("--fragment_retries", "")!! val fragmentRetries = sharedPreferences.getString("--fragment_retries", "")!!
if(retries.isNotEmpty()) request.addOption("retries", retries) if(retries.isNotEmpty()) request.addOption("--retries", retries)
if(fragmentRetries.isNotEmpty()) request.addOption("fragment-retries", fragmentRetries) 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){ if(downloadItem.type != DownloadViewModel.Type.command){
request.addOption("--trim-filenames", 120) request.addOption("--trim-filenames", downDir.absolutePath.length + 120)
if (downloadItem.SaveThumb) { if (downloadItem.SaveThumb) {
request.addOption("--write-thumbnail") request.addOption("--write-thumbnail")
@ -1175,10 +1197,7 @@ class InfoUtil(private val context: Context) {
} }
request.addOption("--embed-chapters") request.addOption("--embed-chapters")
} }
if (downloadItem.videoPreferences.embedSubs) {
request.addOption("--embed-subs")
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" })
}
var cont = "" var cont = ""
val outputContainer = downloadItem.container val outputContainer = downloadItem.container
@ -1302,14 +1321,19 @@ class InfoUtil(private val context: Context) {
val subFormat = sharedPreferences.getString("sub_format", "srt") val subFormat = sharedPreferences.getString("sub_format", "srt")
request.addOption("--write-subs") request.addOption("--write-subs")
request.addOption("--write-auto-subs")
if(subFormat!!.isNotBlank()){ if(subFormat!!.isNotBlank()){
request.addOption("--sub-format", "${subFormat}/best") request.addOption("--sub-format", "${subFormat}/best")
request.addOption("--convert-subtitles", subFormat ?: "srt") 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){ if (downloadItem.videoPreferences.removeAudio){

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

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

@ -10,7 +10,6 @@ import android.widget.Toast
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import com.deniscerri.ytdlnis.R 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.models.LogItem
import com.deniscerri.ytdlnis.database.repository.LogRepository import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel 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.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -39,6 +39,7 @@ class TerminalDownloadWorker(
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
itemId = inputData.getInt("id", 0) itemId = inputData.getInt("id", 0)
val command = inputData.getString("command") val command = inputData.getString("command")
val dao = DBManager.getInstance(context).terminalDao
if (itemId == 0) return Result.failure() if (itemId == 0) return Result.failure()
if (command!!.isEmpty()) return Result.failure() if (command!!.isEmpty()) return Result.failure()
@ -59,10 +60,6 @@ class TerminalDownloadWorker(
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val downloadLocation = sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath()) val downloadLocation = sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
val outputFile = File(context.cacheDir.absolutePath + "/$itemId.txt")
if (! outputFile.exists()) outputFile.createNewFile()
request.addOption( request.addOption(
"--config-locations", "--config-locations",
File(context.cacheDir.absolutePath + "/config-TERMINAL[${System.currentTimeMillis()}].txt").apply { File(context.cacheDir.absolutePath + "/config-TERMINAL[${System.currentTimeMillis()}].txt").apply {
@ -135,9 +132,9 @@ class TerminalDownloadWorker(
} }
return runBlocking { 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)) setProgress(workDataOf("progress" to 100, "output" to it, "id" to itemId, "log" to logDownloads))
Thread.sleep(100) Thread.sleep(50)
} }
if (logDownloads){ if (logDownloads){
@ -146,13 +143,10 @@ class TerminalDownloadWorker(
} }
} }
notificationUtil.cancelDownloadNotification(itemId) notificationUtil.cancelDownloadNotification(itemId)
dao.delete(itemId.toLong())
return@runBlocking Result.success() return@runBlocking Result.success()
} }
}.onFailure { }.onFailure {
if (it is YoutubeDL.CanceledException) {
return Result.failure()
}
return runBlocking { return runBlocking {
it.message?.chunked(10000)?.forEach { it.message?.chunked(10000)?.forEach {
setProgress(workDataOf("progress" to 100, "output" to it, "id" to itemId, "log" to logDownloads)) 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() File(FileUtil.getDefaultCommandPath() + "/" + itemId).deleteRecursively()
Log.e(TAG, context.getString(R.string.failed_download), it) Log.e(TAG, context.getString(R.string.failed_download), it)
notificationUtil.cancelDownloadNotification(itemId) notificationUtil.cancelDownloadNotification(itemId)
dao.delete(itemId.toLong())
return@runBlocking Result.failure() 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"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:liftOnScroll="true"
android:background="@android:color/transparent" 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 <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/custom_command_toolbar" android:id="@+id/custom_command_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="0dp" android:elevation="0dp"
app:title="@string/terminal"
app:menu="@menu/terminal_top_menu" app:menu="@menu/terminal_top_menu"
android:layout_width="match_parent"
app:navigationIcon="@drawable/ic_back" app:navigationIcon="@drawable/ic_back"
android:layout_height="match_parent"/> app:title="@string/terminal" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/frame_layout"
android:layout_width="match_parent" android:name="androidx.navigation.fragment.NavHostFragment"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" android:layout_width="0dp"
android:layout_height="match_parent"> android:layout_height="0dp"
app:defaultNavHost="true"
<LinearLayout app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="match_parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_height="0dp" app:layout_constraintStart_toStartOf="parent"
android:orientation="horizontal" app:layout_constraintTop_toBottomOf="@id/appbar"
app:layout_constraintBottom_toTopOf="@+id/coordinatorLayout" app:navGraph="@navigation/terminal_graph" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> </androidx.constraintlayout.widget.ConstraintLayout>
<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>

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

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?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:scrollbars="none"
android:orientation="vertical" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -22,6 +23,29 @@
android:orientation="horizontal" android:orientation="horizontal"
android:paddingTop="20dp"> 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 <TextView
android:id="@+id/bottom_sheet_title" android:id="@+id/bottom_sheet_title"
@ -37,6 +61,27 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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 <TextView
android:id="@+id/bottom_sheet_subtitle" android:id="@+id/bottom_sheet_subtitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"

@ -49,82 +49,120 @@
android:id="@+id/format_note" android:id="@+id/format_note"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:paddingStart="10dp"
app:layout_constraintHorizontal_bias="0.0"
android:maxLines="2"
android:ellipsize="end" android:ellipsize="end"
android:gravity="start"
android:maxLines="2"
android:textSize="20sp" android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<LinearLayout <RelativeLayout
android:id="@+id/linearLayout" android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginStart="10dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent" 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_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/format_note" app:layout_constraintTop_toBottomOf="@id/format_note">
app:layout_constraintVertical_bias="1.0">
<HorizontalScrollView
<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_width="wrap_content"
android:layout_marginEnd="10dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="5dp" android:layout_marginStart="10dp"
android:background="@drawable/rounded_corner" android:scrollbars="none">
android:backgroundTint="?attr/colorSecondary"
android:clickable="false" <LinearLayout
android:gravity="center" android:id="@+id/linearLayout"
android:minWidth="30dp" android:layout_width="wrap_content"
android:paddingHorizontal="5dp" android:layout_height="wrap_content"
android:textStyle="bold" android:layout_gravity="bottom"
app:cornerRadius="10dp" android:orientation="horizontal">
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" <TextView
app:layout_constraintTop_toTopOf="parent" /> android:id="@+id/audio_formats"
android:layout_width="wrap_content"
</LinearLayout> 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 <TextView
android:id="@+id/format_id" android:id="@+id/format_id"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="3dp"
android:clickable="false" android:clickable="false"
android:gravity="bottom|end" android:gravity="bottom|end"
android:maxLength="10" android:maxLength="10"
android:minWidth="30dp"
app:cornerRadius="10dp" app:cornerRadius="10dp"
app:layout_constraintVertical_bias="1.0"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="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" /> app:layout_constraintTop_toBottomOf="@+id/format_note" />

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent" android:layout_width="match_parent"

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent" android:layout_width="match_parent"

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

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent" android:layout_width="match_parent"

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent" 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" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> 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="" <item android:title=""
android:icon="@drawable/baseline_wrap_text_24" android:icon="@drawable/baseline_wrap_text_24"
app:showAsAction="always" app:showAsAction="always"

@ -66,9 +66,9 @@
</fragment> </fragment>
<activity <activity
android:id="@+id/terminalActivity" 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" android:label="activity_terminal"
tools:layout="@layout/activity_terminal" /> tools:layout="@layout/fragment_terminal" />
<fragment <fragment
android:id="@+id/cookiesFragment" android:id="@+id/cookiesFragment"
android:name="com.deniscerri.ytdlnis.ui.more.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>avc|h264</item>
<item>hev|h265</item> <item>hev|h265</item>
</string-array> </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> </resources>

@ -233,7 +233,7 @@
<string name="finished_download_notification_channel_name">Finished Downloads</string> <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="finished_download_notification_channel_description">Notification for finished or faulty downloads</string>
<string name="quick_download">Quick Download</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="remove_audio">Remove Audio</string>
<string name="preferred_format_id">Preferred Format ID</string> <string name="preferred_format_id">Preferred Format ID</string>
<string name="download_now">Download Now</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">Beta Releases</string>
<string name="update_app_beta_summary">Enroll in the beta program. It may contain bugs.</string> <string name="update_app_beta_summary">Enroll in the beta program. It may contain bugs.</string>
<string name="auto">Auto</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> </resources>

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

@ -20,6 +20,15 @@
app:summary="@string/select_sponsorblock_filtering" app:summary="@string/select_sponsorblock_filtering"
app:title="SponsorBlock" /> 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 <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false" app:defaultValue="false"
@ -145,7 +154,15 @@
android:entryValues="@array/video_codec_values" android:entryValues="@array/video_codec_values"
android:icon="@drawable/ic_code" android:icon="@drawable/ic_code"
app:key="video_codec" 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 <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"

@ -1,18 +1,19 @@
# Project-wide Gradle settings. ## For more details on how to configure your build environment visit
# 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
# http://www.gradle.org/docs/current/userguide/build_environment.html # http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # 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.defaults.buildfeatures.buildconfig=true
android.enableJetifier=true android.enableJetifier=true
android.nonFinalResIds=false android.nonFinalResIds=false
android.nonTransitiveRClass=false android.nonTransitiveRClass=false
android.useAndroidX=true android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1024M" -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

Loading…
Cancel
Save