diff --git a/app/build.gradle b/app/build.gradle index f9888e51..0360c55b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -19,7 +19,7 @@ if (versionBuild > 0){ android { namespace 'com.deniscerri.ytdlnis' - compileSdk 33 + compileSdk 34 try { def propertiesFile = rootProject.file("local.properties") @@ -128,7 +128,7 @@ dependencies { implementation "androidx.appcompat:appcompat:$appCompatVer" implementation "androidx.constraintlayout:constraintlayout:2.1.4" - implementation 'com.google.android.material:material:1.9.0-rc01' + implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.3.1' implementation 'androidx.preference:preference-ktx:1.2.1' diff --git a/app/schemas/com.deniscerri.ytdlnis.database.DBManager/10.json b/app/schemas/com.deniscerri.ytdlnis.database.DBManager/10.json new file mode 100644 index 00000000..385a6ccb --- /dev/null +++ b/app/schemas/com.deniscerri.ytdlnis.database.DBManager/10.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a4b577a0..8c04e1a9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -93,7 +93,7 @@ + + + + (1, TimeUnit.DAYS) - .setConstraints(constraints) - .build() - - WorkManager.getInstance(this@App).enqueueUniquePeriodicWork( - "ytdlp-Updater", - ExistingPeriodicWorkPolicy.KEEP, - periodicWorkRequest - ) - }else{ - WorkManager.getInstance(this@App).cancelUniqueWork("ytdlp-Updater") - } - - - }catch (e: Exception){ Looper.prepare().runCatching { Toast.makeText(this@App, e.message, Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt index 35d934ab..8d263cd6 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt @@ -34,6 +34,7 @@ import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.findNavController import androidx.navigation.ui.setupWithNavController import androidx.preference.PreferenceManager +import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel @@ -51,9 +52,12 @@ import com.google.android.material.elevation.SurfaceColors import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationView import com.google.android.material.navigationrail.NavigationRailView +import com.google.android.material.snackbar.Snackbar +import com.yausername.youtubedl_android.YoutubeDL import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.io.BufferedReader @@ -233,6 +237,19 @@ class MainActivity : BaseActivity() { "Queue" -> navController.navigate(R.id.downloadQueueMainFragment) } + if (sharedPreferences.getBoolean("auto_update_ytdlp", false)){ + CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { + if(DBManager.getInstance(this@MainActivity).downloadDao.getDownloadsCountByStatus(listOf("Active", "Queued")) == 0){ + if (UpdateUtil(this@MainActivity).updateYoutubeDL() == YoutubeDL.UpdateStatus.DONE) { + val version = YoutubeDL.getInstance().version(context) + Snackbar.make(findViewById(android.R.id.content), + this@MainActivity.getString(R.string.ytld_update_success) + " [${version}]", + Snackbar.LENGTH_LONG).show() + } + } + + } + } } override fun onSaveInstanceState(savedInstanceState: Bundle) { @@ -350,8 +367,7 @@ class MainActivity : BaseActivity() { Toast.makeText(context, "Couldn't read file", Toast.LENGTH_LONG).show() return } - val item = downloadViewModel.createEmptyResultItem(f.absolutePath) - val bottomSheet = DownloadBottomSheetDialog(item, DownloadViewModel.Type.command, null, true) + val bottomSheet = DownloadBottomSheetDialog(type = DownloadViewModel.Type.command, result = downloadViewModel.createEmptyResultItem(f.absolutePath)) bottomSheet.show(supportFragmentManager, "downloadSingleSheet") }else{ val `is` = contentResolver.openInputStream(uri!!) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/TerminalDownloadsAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/TerminalDownloadsAdapter.kt new file mode 100644 index 00000000..46ce9e83 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/TerminalDownloadsAdapter.kt @@ -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(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(R.id.progress) + progressBar.tag = "${item.id}##progress" + progressBar.progress = 0 + progressBar.isIndeterminate = true + + // COMMAND ---------------------------------- + val itemTitle = card.findViewById(R.id.title) + itemTitle.text = item.command.trim() + + //OUTPUT + val output = card.findViewById(R.id.output) + output.tag = "${item.id}##output" + + // STOP BUTTON ---------------------------------- + val stopButton = card.findViewById(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 = object : DiffUtil.ItemCallback() { + 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 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt index 70357a75..7b936abc 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt @@ -12,8 +12,18 @@ import kotlinx.coroutines.flow.Flow @TypeConverters(Converters::class) @Database( - entities = [ResultItem::class, HistoryItem::class, DownloadItem::class, CommandTemplate::class, SearchHistoryItem::class, TemplateShortcut::class, CookieItem::class, LogItem::class], - version = 9, + entities = [ + ResultItem::class, + HistoryItem::class, + DownloadItem::class, + CommandTemplate::class, + SearchHistoryItem::class, + TemplateShortcut::class, + CookieItem::class, + LogItem::class, + TerminalItem::class + ], + version = 10, autoMigrations = [ AutoMigration (from = 1, to = 2), AutoMigration (from = 2, to = 3), @@ -22,7 +32,8 @@ import kotlinx.coroutines.flow.Flow AutoMigration (from = 5, to = 6), AutoMigration (from = 6, to = 7), AutoMigration (from = 7, to = 8), - AutoMigration (from = 8, to = 9) + AutoMigration (from = 8, to = 9), + AutoMigration (from = 9, to = 10) ] ) abstract class DBManager : RoomDatabase(){ @@ -33,6 +44,7 @@ abstract class DBManager : RoomDatabase(){ abstract val searchHistoryDao: SearchHistoryDao abstract val cookieDao: CookieDao abstract val logDao: LogDao + abstract val terminalDao: TerminalDao companion object { //prevents multiple instances of db getting created at the same time diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt index eabfa80b..3fedfefe 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt @@ -20,7 +20,10 @@ interface DownloadDao { fun getActiveDownloadsCountFlow() : Flow @Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)") - fun getDownloadsCountByStatus(status : List) : Flow + fun getDownloadsCountByStatusFlow(status : List) : Flow + + @Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)") + fun getDownloadsCountByStatus(status : List) : Int @Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'") fun getActiveAndPausedDownloadsList() : List diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/TerminalDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/TerminalDao.kt new file mode 100644 index 00000000..f9ccf873 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/TerminalDao.kt @@ -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 + + @Query("SELECT * FROM terminalDownloads ORDER BY id") + fun getActiveTerminalDownloadsFlow() : Flow> + + @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) +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/TerminalItem.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/TerminalItem.kt new file mode 100644 index 00000000..fc5433a4 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/TerminalItem.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt index 128d2763..4d8d8589 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt @@ -33,36 +33,28 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co suspend fun search(inputQuery: String, resetResults: Boolean) : ArrayList{ val infoUtil = InfoUtil(context) - try{ - if (resetResults) deleteAll() - val res = infoUtil.search(inputQuery) - itemCount.value = res.size - res.forEach { - resultDao.insert(it!!) - } - return res - }catch (ignored: Exception){} - return arrayListOf() + if (resetResults) deleteAll() + val res = infoUtil.search(inputQuery) + itemCount.value = res.size + res.forEach { + resultDao.insert(it!!) + } + return res } suspend fun getOne(inputQuery: String, resetResults: Boolean) : ArrayList{ val infoUtil = InfoUtil(context) - try { - val v = infoUtil.getYoutubeVideo(inputQuery) - if (resetResults) { - deleteAll() - itemCount.value = v.size - }else{ - v.forEach { it?.playlistTitle = "ytdlnis-Search" } - } - v.forEach { - resultDao.insert(it!!) - } - return ArrayList(v) - } catch (e: Exception) { - Log.e(tag, e.toString()) + val v = infoUtil.getYoutubeVideo(inputQuery) + if (resetResults) { + deleteAll() + itemCount.value = v.size + }else{ + v.forEach { it?.playlistTitle = "ytdlnis-Search" } } - return arrayListOf() + v.forEach { + resultDao.insert(it!!) + } + return ArrayList(v) } suspend fun getPlaylist(inputQuery: String, resetResults: Boolean) : ArrayList{ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt index e82b5f2a..694fa1af 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt @@ -46,13 +46,9 @@ import com.google.gson.Gson import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.io.File -import java.text.SimpleDateFormat -import java.util.Calendar import java.util.Locale import java.util.concurrent.TimeUnit @@ -84,6 +80,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application private var audioContainer: String? private var videoContainer: String? private var videoCodec: String? + private var audioCodec: String? private val dao: DownloadDao enum class Type { @@ -155,6 +152,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } videoCodec = sharedPreferences.getString("video_codec", "") + audioCodec = sharedPreferences.getString("audio_codec", "") } fun deleteDownload(id: Long) = viewModelScope.launch(Dispatchers.IO) { @@ -188,7 +186,9 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } } - fun createDownloadItemFromResult(resultItem: ResultItem, givenType: Type) : DownloadItem { + fun createDownloadItemFromResult(result: ResultItem?, url: String = "", givenType: Type) : DownloadItem { + val resultItem = result ?: createEmptyResultItem(url) + val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false) val saveSubs = sharedPreferences.getBoolean("write_subtitles", false) val addChapters = sharedPreferences.getBoolean("add_chapters", false) @@ -386,12 +386,12 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application Type.audio -> { return cloneFormat ( try { - try{ - formats.filter { it.format_note.contains("audio", ignoreCase = true)} - .firstOrNull { audioFormatIDPreference.indexOfFirst { a -> a.contains(it.format_id) } >= 0 || it.container == audioContainer} ?: throw Exception() - }catch (e: Exception){ - formats.first { it.format_note.contains("audio", ignoreCase = true) } - } + val theFormats = formats.filter { it.format_note.contains("audio", ignoreCase = true) } + val requirements: MutableList<(Format) -> Boolean> = mutableListOf() + requirements.add {it: Format -> audioFormatIDPreference.contains(it.format_id)} + requirements.add {it: Format -> it.container == audioContainer } + requirements.add {it: Format -> "^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)} + theFormats.maxByOrNull { f -> requirements.count{req -> req(f)} } ?: throw Exception() }catch (e: Exception){ bestAudioFormat } @@ -408,7 +408,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } else /*best*/ -> { val requirements: MutableList<(Format) -> Boolean> = mutableListOf() - requirements.add { it: Format -> formatIDPreference.sorted().contains(it.format_id) } + requirements.add { it: Format -> formatIDPreference.contains(it.format_id) } requirements.add { it: Format -> if (videoContainer == "mp4") it.container.equals("mpeg_4", true) else it.container.equals(videoContainer, true)} requirements.add { it: Format -> it.format_note.contains(videoQualityPreference.split("_")[0].dropLast(1)) } requirements.add { it: Format -> "^(${videoCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)} @@ -451,7 +451,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application var preferredType = sharedPreferences.getString("preferred_download_type", "video") items.forEach { preferredType = getDownloadType(Type.valueOf(preferredType!!), it!!.url).toString() - list.add(createDownloadItemFromResult(it, Type.valueOf(preferredType!!))) + list.add(createDownloadItemFromResult(result = it, givenType = Type.valueOf(preferredType!!))) } return list } @@ -656,7 +656,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } fun getTotalSize(status: List) : LiveData { - return dbManager.downloadDao.getDownloadsCountByStatus(status.map { it.toString() }).asLiveData() + return dbManager.downloadDao.getDownloadsCountByStatusFlow(status.map { it.toString() }).asLiveData() } fun checkAllQueuedItemsAreScheduledAfterNow(items: List, inverted: Boolean, currentStartTime: Long) : Boolean { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt index e5f234f1..32862b43 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt @@ -64,18 +64,20 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) deleteAll() } } - suspend fun parseQueries(inputQueries: List){ + suspend fun parseQueries(inputQueries: List) : MutableList{ state = ResultsState.PROCESSING - if (inputQueries.size == 1){ + return if (inputQueries.size == 1){ parseQuery(inputQueries[0], true) }else { repository.itemCount.value = inputQueries.size loadingItems.postValue(true) + val items = mutableListOf() inputQueries.forEach { - parseQuery(it, false) + items.addAll(parseQuery(it, false)) } state = ResultsState.IDLE loadingItems.postValue(false) + items } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/TerminalViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/TerminalViewModel.kt new file mode 100644 index 00000000..736ca623 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/TerminalViewModel.kt @@ -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> { + 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() + .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) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt index aae9bdf3..a51e66d8 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt @@ -108,17 +108,6 @@ class ShareActivity : BaseActivity() { runCatching { supportFragmentManager.popBackStack() } quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false) || sharedPreferences.getString("preferred_download_type", "video") == "command") - - val loadingBottomSheet = BottomSheetDialog(this) - loadingBottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) - loadingBottomSheet.setContentView(R.layout.please_wait_bottom_sheet) - loadingBottomSheet.show() - val cancel = loadingBottomSheet.findViewById(R.id.cancel) - cancel!!.setOnClickListener { - resultViewModel.deleteAll() - this.finish() - } - val intentData = when(action){ Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)!! else -> intent.dataString!! @@ -132,15 +121,16 @@ class ShareActivity : BaseActivity() { lifecycleScope.launch { try { - val result = resultViewModel.getItemByURL(inputQuery) - loadingBottomSheet.dismiss() + val result = withContext(Dispatchers.IO){ + resultViewModel.getItemByURL(inputQuery) + } + if (result == null) throw Exception() showDownloadSheet(result) }catch (e: Exception){ resultViewModel.deleteAll() - if (quickDownload && matcher.matches()){ - val result = downloadViewModel.createEmptyResultItem(inputQuery) - loadingBottomSheet.dismiss() - showDownloadSheet(result) + showDownloadSheet(downloadViewModel.createEmptyResultItem(inputQuery)) + if (matcher.matches()){ + }else{ val res = withContext(Dispatchers.IO){ resultViewModel.parseQuery(inputQuery, true) @@ -149,7 +139,6 @@ class ShareActivity : BaseActivity() { Toast.makeText(this@ShareActivity, "No Results Found!", Toast.LENGTH_SHORT).show() exit() }else{ - loadingBottomSheet.dismiss() if (res.size == 1){ showDownloadSheet(res[0]!!) }else{ @@ -163,12 +152,17 @@ class ShareActivity : BaseActivity() { } private fun showDownloadSheet(it: ResultItem){ + val downloadType = downloadViewModel.getDownloadType(DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!), it.url) if (sharedPreferences.getBoolean("download_card", true)){ - val bottomSheet = DownloadBottomSheetDialog(it,downloadViewModel.getDownloadType(DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!), it.url), null, quickDownload) + val bottomSheet = DownloadBottomSheetDialog( + result = it, + type = downloadType) bottomSheet.show(supportFragmentManager, "downloadSingleSheet") }else{ lifecycleScope.launch(Dispatchers.IO){ - val downloadItem = downloadViewModel.createDownloadItemFromResult(it, DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!)) + val downloadItem = downloadViewModel.createDownloadItemFromResult( + result = it, + givenType = downloadType) downloadViewModel.queueDownloads(listOf(downloadItem)) } this.finish() @@ -184,7 +178,9 @@ class ShareActivity : BaseActivity() { val downloadItems = mutableListOf() lifecycleScope.launch(Dispatchers.IO){ it.forEach { res -> - val i = downloadViewModel.createDownloadItemFromResult(res!!, DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!)) + val i = downloadViewModel.createDownloadItemFromResult( + result = res!!, + givenType = DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!)) i.format = downloadViewModel.getLatestCommandTemplateAsFormat() downloadItems.add(i) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/ErrorDialogActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/ErrorDialogActivity.kt new file mode 100644 index 00000000..baaaa980 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/ErrorDialogActivity.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt index 1c9483ea..c71affa0 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt @@ -6,8 +6,6 @@ import android.content.* import android.content.Context.CLIPBOARD_SERVICE import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.drawable.ShapeDrawable -import android.graphics.drawable.shapes.OvalShape import android.os.Bundle import android.os.Handler import android.os.Looper @@ -23,8 +21,6 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.children import androidx.core.view.forEach -import androidx.core.view.forEachIndexed -import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider @@ -45,7 +41,6 @@ import com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.ThemeUtil import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll -import com.devbrackets.android.exomedia.core.video.mp.NativeVideoDelegate import com.facebook.shimmer.ShimmerFrameLayout import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar @@ -61,7 +56,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import me.zhanghai.android.fastscroll.FastScrollerBuilder import java.util.* @@ -168,7 +162,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene }else if (resultViewModel.repository.itemCount.value == 1){ if (sharedPreferences!!.getBoolean("download_card", true)){ if(it.size == 1 && quickLaunchSheet && parentFragmentManager.findFragmentByTag("downloadSingleSheet") == null){ - showSingleDownloadSheet(it[0], DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), false) + showSingleDownloadSheet( + it[0], + DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!) + ) } } }else{ @@ -212,7 +209,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene if (arguments?.getString("url") != null){ val url = requireArguments().getString("url") if (inputQueries == null) inputQueries = mutableListOf() - searchBar?.text = url + searchBar?.setText(url) val argList = url!!.split("\n").toMutableList() argList.removeAll(listOf("", null)) inputQueries!!.addAll(argList) @@ -242,18 +239,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene } } - if (arguments?.getBoolean("showDownloadsWithUpdatedFormats") == true){ - CoroutineScope(Dispatchers.IO).launch { - val ids = arguments?.getLongArray("downloadIds") - val items = mutableListOf() - ids?.forEach { - items.add(downloadViewModel.getItemByID(it)) - } - val bottomSheet = DownloadMultipleBottomSheetDialog(items.toMutableList()) - bottomSheet.show(parentFragmentManager, "downloadMultipleSheet") - } - } - } override fun onResume() { @@ -267,7 +252,20 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene }else{ arguments?.remove("url") } - arguments?.remove("showDownloadsWithUpdatedFormats") + + if (arguments?.getBoolean("showDownloadsWithUpdatedFormats") == true){ + arguments?.remove("showDownloadsWithUpdatedFormats") + CoroutineScope(Dispatchers.IO).launch { + val ids = arguments?.getLongArray("downloadIds") + val items = mutableListOf() + ids?.forEach { + items.add(downloadViewModel.getItemByID(it)) + } + val bottomSheet = DownloadMultipleBottomSheetDialog(items.toMutableList()) + bottomSheet.show(parentFragmentManager, "downloadMultipleSheet") + } + } + if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){ lifecycleScope.launch { updateSearchViewItems(searchView?.editText?.text, linkYouCopied) @@ -412,7 +410,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene R.id.delete_results -> { resultViewModel.getTrending() selectedObjects = ArrayList() - searchBar!!.text = "" + searchBar!!.setText("") downloadAllFabCoordinator!!.visibility = GONE downloadFabs!!.visibility = GONE } @@ -538,7 +536,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene if (queryList.isEmpty()) return if (queryList.size == 1){ - searchBar!!.text = searchView.text + searchBar!!.setText(searchView.text) } searchView.hide() @@ -552,13 +550,18 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene Thread.sleep(300) if(sharedPreferences!!.getBoolean("quick_download", false) || sharedPreferences!!.getString("preferred_download_type", "video") == "command"){ if (queryList.size == 1 && queryList.first().matches("(https?://(?:www\\.|(?!www))[a-zA-Z\\d][a-zA-Z\\d-]+[a-zA-Z\\d]\\.\\S{2,}|www\\.[a-zA-Z\\d][a-zA-Z\\d-]+[a-zA-Z\\d]\\.\\S{2,}|https?://(?:www\\.|(?!www))[a-zA-Z\\d]+\\.\\S{2,}|www\\.[a-zA-Z\\d]+\\.\\S{2,})".toRegex())){ - val resultItem = downloadViewModel.createEmptyResultItem(queryList.first()) if (sharedPreferences!!.getBoolean("download_card", true)) { - showSingleDownloadSheet(resultItem, DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), true) + showSingleDownloadSheet( + resultItem = downloadViewModel.createEmptyResultItem(queryList.first()), + type = DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!) + ) } else { lifecycleScope.launch{ val downloadItem = withContext(Dispatchers.IO){ - downloadViewModel.createDownloadItemFromResult(resultItem, DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!)) + downloadViewModel.createDownloadItemFromResult( + result = downloadViewModel.createEmptyResultItem(queryList.first()), + givenType = DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!) + ) } downloadViewModel.queueDownloads(listOf(downloadItem)) } @@ -585,11 +588,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene Log.e(TAG, resultsList!![0].toString() + " " + videoURL) recyclerView!!.findViewWithTag("""${item?.url}##$type""") if (sharedPreferences!!.getBoolean("download_card", true)) { - showSingleDownloadSheet(item!!, type!!, false) + showSingleDownloadSheet(item!!, type!!) } else { lifecycleScope.launch{ val downloadItem = withContext(Dispatchers.IO){ - downloadViewModel.createDownloadItemFromResult(item!!, type!!) + downloadViewModel.createDownloadItemFromResult( + result = item!!, + givenType = type!!) } downloadViewModel.queueDownloads(listOf(downloadItem)) } @@ -599,11 +604,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene override fun onLongButtonClick(videoURL: String, type: DownloadViewModel.Type?) { Log.e(TAG, type.toString() + " " + videoURL) val item = resultsList!!.find { it?.url == videoURL } - showSingleDownloadSheet(item!!, type!!, false) + showSingleDownloadSheet(item!!, type!!) } - private fun showSingleDownloadSheet(resultItem : ResultItem, type: DownloadViewModel.Type, quickDownload: Boolean){ - val bottomSheet = DownloadBottomSheetDialog(resultItem, downloadViewModel.getDownloadType(type, resultItem.url), null, quickDownload) + private fun showSingleDownloadSheet( + resultItem: ResultItem, + type: DownloadViewModel.Type + ){ + val bottomSheet = DownloadBottomSheetDialog(resultItem, downloadViewModel.getDownloadType(type, resultItem.url)) bottomSheet.show(parentFragmentManager, "downloadSingleSheet") } @@ -696,7 +704,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene R.id.download -> { lifecycleScope.launch { if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects!!.size == 1) { - showSingleDownloadSheet(selectedObjects!![0], DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), false) + showSingleDownloadSheet( + selectedObjects!![0], + DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!) + ) }else{ val downloadList = withContext(Dispatchers.IO){ downloadViewModel.turnResultItemsToDownloadItems(selectedObjects!!) @@ -704,7 +715,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene if (sharedPreferences!!.getBoolean("download_card", true)) { if (selectedObjects!!.size == 1){ - showSingleDownloadSheet(selectedObjects!![0], DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), false) + showSingleDownloadSheet( + selectedObjects!![0], + DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!) + ) }else{ val bottomSheet = DownloadMultipleBottomSheetDialog(downloadList.toMutableList()) bottomSheet.show(parentFragmentManager, "downloadMultipleSheet") diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/AddExtraCommandsDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/AddExtraCommandsDialog.kt index fbcce7b7..060a69ab 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/AddExtraCommandsDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/AddExtraCommandsDialog.kt @@ -77,6 +77,7 @@ class AddExtraCommandsDialog(private val item: DownloadItem?, private val callba } + @SuppressLint("SetTextI18n") @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -143,9 +144,14 @@ class AddExtraCommandsDialog(private val item: DownloadItem?, private val callba shortcuts.setOnClickListener { lifecycleScope.launch { if (shortcutCount > 0){ - UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel) {sh -> - text.text.insert(text.selectionStart, " $sh ") - } + UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel, + itemSelected = {sh -> + text.setText("${text.text} $sh") + }, + itemRemoved = {removed -> + text.setText(text.text.replace("(${Regex.escape(removed)})(?!.*\\1)".toRegex(), "")) + text.setSelection(text.text.length) + }) } } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt index f13fe9b1..baef007a 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt @@ -5,7 +5,6 @@ import android.app.Dialog import android.content.DialogInterface import android.os.Bundle import android.util.DisplayMetrics -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -24,19 +23,16 @@ import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel -import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.UiUtil import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.tabs.TabLayout -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext - -class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, private val downloadItem: DownloadItem, private val listener: OnDownloadItemUpdateListener) : BottomSheetDialogFragment() { +//TODO MAKE CONFIGURE LIKE NORMAL SHEET +class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private val currentDownloadItem: DownloadItem, private val listener: OnDownloadItemUpdateListener) : BottomSheetDialogFragment() { private lateinit var tabLayout: TabLayout private lateinit var viewPager2: ViewPager2 private lateinit var fragmentAdapter : DownloadFragmentAdapter @@ -80,17 +76,12 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri overScrollMode = View.OVER_SCROLL_NEVER } - val fragments = mutableListOf( - DownloadAudioFragment(resultItem, downloadItem), - DownloadVideoFragment(resultItem, downloadItem) - ) + var commandTemplateNr = 0 lifecycleScope.launch{ withContext(Dispatchers.IO){ commandTemplateNr = commandTemplateDao.getTotalNumber() - if(commandTemplateNr > 0){ - fragments.add(DownloadCommandFragment(resultItem, downloadItem)) - }else{ + if(commandTemplateNr <= 0){ (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.alpha = 0.3f } @@ -98,7 +89,7 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri } //check if the item has formats and its audio-only - val isAudioOnly = resultItem.formats.isNotEmpty() && resultItem.formats.none { !it.format_note.contains("audio") } + val isAudioOnly = result.formats.isNotEmpty() && result.formats.none { !it.format_note.contains("audio") } if (isAudioOnly){ (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f @@ -107,15 +98,15 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri val fragmentManager = parentFragmentManager fragmentAdapter = DownloadFragmentAdapter( - resultItem, fragmentManager, lifecycle, - fragments + result, + currentDownloadItem ) viewPager2.adapter = fragmentAdapter viewPager2.isSaveFromParentEnabled = false - when(downloadItem.type) { + when(currentDownloadItem.type) { Type.audio -> { tabLayout.selectTab(tabLayout.getTabAt(0)) viewPager2.setCurrentItem(0, false) @@ -172,17 +163,17 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri val ok = view.findViewById