From e91262cc66cca67ff3d3000ef65ba3f560a9695e Mon Sep 17 00:00:00 2001 From: deniscerri <64997243+deniscerri@users.noreply.github.com> Date: Sun, 20 Aug 2023 17:01:39 +0200 Subject: [PATCH] fixed webview ok button slightly improved listing of items if u have alot of items fixed app not enabling ok button in the cut screen if you change the TO text box only added force keyframes at cuts switch in the cut screen Added 3 dots in multiple download card. It will have the configure chips in a separate card. Each will have a callback that will update all items in multiple download fixed start end textboxes showing truncanted text in playlist select card add ability to start a download now and put the rest of the queue behind it Scrollbar handle shrinked depending on size. Made it same size Fixed Tapping the notification of the errored download leading to the running tab instead of errored Set different icon for terminal in share menu ignored extra command preference in backups of command templates made preference titles be multiline if they are too long if the preferred download type is command, the app will not bother fetching data beforehand fixed app not showing the entire interface when being in landscape hid format details in command type logs because they are useless fixed cookies import not working from external sources other than the app fixed app using continue button for a split second when there are no paused downloads added state in home screen so that it wont populate trending videos while its searching for an item implemented batch downloads in a single worker to avoid bogging down the system with many works fixed instagram status that have multiple videos just using the first video fixed monochrome icon not showing made yt-dlp auto update a choice in the settings fixed bilibili not working in normal mode fixed app overwriting files instead of adding (1) in the filename instead --- .idea/misc.xml | 2 +- app/build.gradle | 28 +- app/src/main/AndroidManifest.xml | 27 +- .../main/java/com/deniscerri/ytdlnis/App.kt | 43 +- .../ytdlnis/adapter/ActiveDownloadAdapter.kt | 11 +- .../ytdlnis/adapter/GenericDownloadAdapter.kt | 78 +-- .../ytdlnis/adapter/PlaylistAdapter.kt | 15 +- .../deniscerri/ytdlnis/database/Converters.kt | 18 +- .../deniscerri/ytdlnis/database/DBManager.kt | 5 + .../ytdlnis/database/dao/DownloadDao.kt | 88 ++- .../ytdlnis/database/dao/SearchHistoryDao.kt | 2 +- .../ytdlnis/database/models/DownloadItem.kt | 2 +- .../ytdlnis/database/models/ResultItem.kt | 4 +- .../database/repository/DownloadRepository.kt | 57 +- .../database/viewmodel/CookieViewModel.kt | 19 +- .../database/viewmodel/DownloadViewModel.kt | 223 ++++--- .../database/viewmodel/ResultViewModel.kt | 14 +- .../CancelDownloadNotificationReceiver.kt | 4 +- .../PauseDownloadNotificationReceiver.kt | 13 + .../ytdlnis/receiver/ShareActivity.kt | 17 +- .../com/deniscerri/ytdlnis/ui/HomeFragment.kt | 17 +- .../ui/downloadcard/AddExtraCommandsDialog.kt | 16 +- .../downloadcard/CutVideoBottomSheetDialog.kt | 88 ++- .../ui/downloadcard/DownloadAudioFragment.kt | 149 +---- .../downloadcard/DownloadBottomSheetDialog.kt | 4 +- .../downloadcard/DownloadCommandFragment.kt | 72 +-- .../DownloadMultipleBottomSheetDialog.kt | 203 +++++-- .../ui/downloadcard/DownloadVideoFragment.kt | 218 ++----- .../FormatSelectionBottomSheetDialog.kt | 16 +- .../downloadcard/ResultCardDetailsDialog.kt | 475 ++++++++------- ...Dialog.kt => SelectPlaylistItemsDialog.kt} | 83 ++- .../ui/downloads/ActiveDownloadsFragment.kt | 158 ++--- .../downloads/CancelledDownloadsFragment.kt | 293 ++++----- .../ui/downloads/DownloadQueueMainFragment.kt | 5 +- .../ui/downloads/ErroredDownloadsFragment.kt | 297 ++++----- .../ytdlnis/ui/downloads/HistoryFragment.kt | 2 + .../ui/downloads/QueuedDownloadsFragment.kt | 401 ++++++------- .../ui/downloads/SavedDownloadsFragment.kt | 295 ++++----- .../ytdlnis/ui/more/CookiesFragment.kt | 1 + .../ytdlnis/ui/more/WebViewActivity.kt | 2 + .../more/settings/DownloadSettingsFragment.kt | 22 - .../more/settings/FolderSettingsFragment.kt | 6 +- .../ui/more/settings/MainSettingsFragment.kt | 3 +- .../com/deniscerri/ytdlnis/util/FileUtil.kt | 69 ++- .../com/deniscerri/ytdlnis/util/InfoUtil.kt | 90 +-- .../ytdlnis/util/NotificationUtil.kt | 38 ++ .../com/deniscerri/ytdlnis/util/UiUtil.kt | 565 +++++++++++++++++- .../ytdlnis/util/VideoPlayerUtil.kt | 19 - .../deniscerri/ytdlnis/work/DownloadWorker.kt | 356 +++++------ .../ytdlnis/work/TerminalDownloadWorker.kt | 53 +- .../main/res/drawable/baseline_close_24.xml | 5 + .../baseline_desktop_access_disabled_24.xml | 5 + .../drawable/baseline_desktop_windows_24.xml | 5 + app/src/main/res/drawable/ic_more.xml | 2 +- .../res/drawable/terminal_icon_background.xml | 10 + .../res/drawable/terminal_icon_foreground.xml | 15 + .../main/res/layout-land/activity_main.xml | 61 ++ .../main/res/layout-land/fragment_home.xml | 312 ++++++++++ .../res/layout-land/result_card_details.xml | 172 ++++++ .../res/layout-sw600dp-land/fragment_home.xml | 5 - .../main/res/layout-sw600dp/fragment_home.xml | 6 - .../res/layout-sw950dp-land/fragment_home.xml | 5 - app/src/main/res/layout/adjust_audio.xml | 126 ++++ app/src/main/res/layout/adjust_command.xml | 45 ++ app/src/main/res/layout/adjust_video.xml | 205 +++++++ app/src/main/res/layout/cut_video_sheet.xml | 10 + app/src/main/res/layout/download_card.xml | 1 - .../layout/download_multiple_bottom_sheet.xml | 5 - .../layout/extra_commands_bottom_sheet.xml | 1 + .../main/res/layout/format_details_sheet.xml | 141 ++--- app/src/main/res/layout/fragment_active.xml | 2 +- .../res/layout/fragment_download_audio.xml | 126 +--- .../res/layout/fragment_download_command.xml | 45 +- .../res/layout/fragment_download_video.xml | 204 +------ app/src/main/res/layout/fragment_history.xml | 5 - app/src/main/res/layout/fragment_home.xml | 6 - app/src/main/res/layout/fragment_inqueue.xml | 12 +- app/src/main/res/layout/playlist_item.xml | 23 +- .../main/res/layout/result_card_details.xml | 39 +- .../main/res/layout/select_playlist_items.xml | 116 ++-- .../main/res/menu/download_multiple_menu.xml | 13 +- .../main/res/menu/playlist_download_menu.xml | 9 + app/src/main/res/menu/queued_menu_context.xml | 7 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 1 + .../ic_launcher_round_dark.xml | 1 + .../ic_launcher_round_light.xml | 1 + .../res/mipmap-anydpi-v26/terminal_icon.xml | 5 + .../mipmap-anydpi-v26/terminal_icon_round.xml | 5 + .../main/res/mipmap-hdpi/terminal_icon.webp | Bin 0 -> 1896 bytes .../res/mipmap-hdpi/terminal_icon_round.webp | Bin 0 -> 1896 bytes .../main/res/mipmap-mdpi/terminal_icon.webp | Bin 0 -> 1168 bytes .../res/mipmap-mdpi/terminal_icon_round.webp | Bin 0 -> 1168 bytes .../main/res/mipmap-xhdpi/terminal_icon.webp | Bin 0 -> 2766 bytes .../res/mipmap-xhdpi/terminal_icon_round.webp | Bin 0 -> 2766 bytes .../main/res/mipmap-xxhdpi/terminal_icon.webp | Bin 0 -> 4380 bytes .../mipmap-xxhdpi/terminal_icon_round.webp | Bin 0 -> 4380 bytes .../res/mipmap-xxxhdpi/terminal_icon.webp | Bin 0 -> 6372 bytes .../mipmap-xxxhdpi/terminal_icon_round.webp | Bin 0 -> 6372 bytes app/src/main/res/navigation/nav_graph.xml | 10 +- app/src/main/res/values/arrays.xml | 22 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/styles.xml | 6 + app/src/main/res/xml/folders_preference.xml | 1 + .../main/res/xml/processing_preferences.xml | 20 +- app/src/main/res/xml/updating_preferences.xml | 11 +- app/src/main/terminal_icon-playstore.png | Bin 0 -> 9680 bytes build.gradle | 9 +- gradle.properties | 2 +- 108 files changed, 3873 insertions(+), 2653 deletions(-) rename app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/{SelectPlaylistItemsBottomSheetDialog.kt => SelectPlaylistItemsDialog.kt} (74%) create mode 100644 app/src/main/res/drawable/baseline_close_24.xml create mode 100644 app/src/main/res/drawable/baseline_desktop_access_disabled_24.xml create mode 100644 app/src/main/res/drawable/baseline_desktop_windows_24.xml create mode 100644 app/src/main/res/drawable/terminal_icon_background.xml create mode 100644 app/src/main/res/drawable/terminal_icon_foreground.xml create mode 100644 app/src/main/res/layout-land/activity_main.xml create mode 100644 app/src/main/res/layout-land/fragment_home.xml create mode 100644 app/src/main/res/layout-land/result_card_details.xml create mode 100644 app/src/main/res/layout/adjust_audio.xml create mode 100644 app/src/main/res/layout/adjust_command.xml create mode 100644 app/src/main/res/layout/adjust_video.xml create mode 100644 app/src/main/res/menu/playlist_download_menu.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/terminal_icon.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/terminal_icon_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/terminal_icon.webp create mode 100644 app/src/main/res/mipmap-hdpi/terminal_icon_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/terminal_icon.webp create mode 100644 app/src/main/res/mipmap-mdpi/terminal_icon_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/terminal_icon.webp create mode 100644 app/src/main/res/mipmap-xhdpi/terminal_icon_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/terminal_icon.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/terminal_icon_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/terminal_icon.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/terminal_icon_round.webp create mode 100644 app/src/main/terminal_icon-playstore.png diff --git a/.idea/misc.xml b/.idea/misc.xml index f58d8de1..34572e74 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index b4b2222b..09936bfe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,15 +2,15 @@ plugins { id 'com.android.application' id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1' id 'org.jetbrains.kotlin.android' - id 'kotlin-kapt' + id 'com.google.devtools.ksp' id "org.jetbrains.kotlin.plugin.serialization" version "1.8.10" } def properties = new Properties() def versionMajor = 1 def versionMinor = 6 -def versionPatch = 3 -def versionBuild = 17 // bump for dogfood builds, public betas, etc. +def versionPatch = 5 +def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionExt = "" if (versionBuild > 0){ @@ -49,10 +49,8 @@ android { useSupportLibrary true } - kapt { - arguments { - arg("room.schemaLocation", "$projectDir/schemas") - } + ksp { + arg("room.schemaLocation", "$projectDir/schemas") } } @@ -76,6 +74,8 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' debuggable false signingConfig signingConfigs.debug + applicationIdSuffix '.beta' + versionNameSuffix '-Beta' } debug { @@ -98,7 +98,7 @@ android { disable 'MissingTranslation' } - android.applicationVariants.all { variant -> + android.applicationVariants.configureEach { variant -> variant.outputs.each { output -> def baseAbiVersionCode = project.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI)) if (baseAbiVersionCode != null) { @@ -149,7 +149,7 @@ dependencies { implementation 'com.google.android.material:material:1.9.0-rc01' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.3.1' - implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.preference:preference-ktx:1.2.1' implementation "androidx.navigation:navigation-fragment-ktx:$navVer" implementation "androidx.navigation:navigation-ui-ktx:$navVer" implementation 'androidx.core:core-ktx:1.10.1' @@ -174,16 +174,18 @@ dependencies { implementation "androidx.room:room-runtime:2.5.2" implementation "androidx.room:room-ktx:2.5.2" - kapt "androidx.room:room-compiler:2.5.2" + ksp "androidx.room:room-compiler:2.5.2" + implementation 'androidx.paging:paging-runtime-ktx:3.2.0' + implementation "androidx.room:room-paging:2.5.2" androidTestImplementation "androidx.room:room-testing:2.5.2" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' implementation "androidx.compose.runtime:runtime:$composeVer" - androidTestImplementation "com.google.truth:truth:1.1.3" + androidTestImplementation 'com.google.truth:truth:1.1.5' api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVer" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVer" - implementation 'com.google.code.gson:gson:2.9.1' + implementation 'com.google.code.gson:gson:2.10.1' implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.10" implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' @@ -200,7 +202,7 @@ dependencies { implementation("androidx.media3:media3-ui:$media3_version") // For RTSP playback support with ExoPlayer implementation("androidx.media3:media3-exoplayer-rtsp:$media3_version") - implementation("androidx.media3:media3-datasource-cronet:$media3_version") implementation "com.anggrayudi:storage:1.5.5" + implementation 'me.zhanghai.android.fastscroll:library:1.3.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 91a929f8..94e2d12e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,17 @@ + + + + + + + + + + + @@ -204,7 +217,7 @@ @@ -212,18 +225,6 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" /> - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/App.kt b/app/src/main/java/com/deniscerri/ytdlnis/App.kt index 958013c7..1fbf7f9a 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/App.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/App.kt @@ -9,14 +9,11 @@ import androidx.work.Configuration import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequest import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager -import androidx.work.WorkQuery import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.UpdateUtil import com.deniscerri.ytdlnis.work.UpdateYTDLWorker -import com.google.android.gms.common.internal.Constants import com.yausername.aria2c.Aria2c import com.yausername.ffmpeg.FFmpeg import com.yausername.youtubedl_android.YoutubeDL @@ -50,21 +47,27 @@ class App : Application() { } } - //init yt-dlp auto update with work request - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.UNMETERED) - .setRequiresDeviceIdle(true) - .build() + 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(1, TimeUnit.DAYS) + .setConstraints(constraints) + .build() + + WorkManager.getInstance(this@App).enqueueUniquePeriodicWork( + "ytdlp-Updater", + ExistingPeriodicWorkPolicy.KEEP, + periodicWorkRequest + ) + }else{ + WorkManager.getInstance(this@App).cancelUniqueWork("ytdlp-Updater") + } - val periodicWorkRequest = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) - .setConstraints(constraints) - .build() - WorkManager.getInstance(this@App).enqueueUniquePeriodicWork( - "ytdlp-Updater", - ExistingPeriodicWorkPolicy.KEEP, - periodicWorkRequest - ) }catch (e: Exception){ Looper.prepare().runCatching { @@ -73,14 +76,6 @@ class App : Application() { e.printStackTrace() } } - - WorkManager.initialize( - this@App, - Configuration.Builder() - .setExecutor(Executors.newFixedThreadPool( - sharedPreferences.getInt("concurrent_downloads", 1))) - .build()) - } @Throws(YoutubeDLException::class) private fun initLibraries() { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/ActiveDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/ActiveDownloadAdapter.kt index c7deb943..7ef835dc 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/ActiveDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/ActiveDownloadAdapter.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import android.graphics.Color import android.os.Handler import android.os.Looper +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -106,14 +107,14 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: } val formatDetailsChip = card.findViewById(R.id.format_note) - val formatDetailsText = StringBuilder(item.format.format_note.uppercase()) - formatDetailsText.append(" \t •\t ${item.container.uppercase().ifEmpty { item.format.container.uppercase() }}") + val sideDetails = mutableListOf() + sideDetails.add(item.format.format_note.uppercase()) + sideDetails.add(item.container.uppercase().ifEmpty { item.format.container.uppercase() }) val fileSize = FileUtil.convertFileSize(item.format.filesize) - if (fileSize != "?") formatDetailsText.append(" \t •\t $fileSize") - - formatDetailsChip.text = formatDetailsText + if (fileSize != "?") sideDetails.add(fileSize) + formatDetailsChip.text = sideDetails.joinToString(" · ") //OUTPUT val output = card.findViewById(R.id.output) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/GenericDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/GenericDownloadAdapter.kt index 6e171ea0..1071d67f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/GenericDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/GenericDownloadAdapter.kt @@ -3,7 +3,6 @@ package com.deniscerri.ytdlnis.adapter import android.annotation.SuppressLint import android.app.Activity import android.content.SharedPreferences -import android.graphics.Color import android.os.Handler import android.os.Looper import android.view.LayoutInflater @@ -11,10 +10,10 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible +import androidx.paging.PagingDataAdapter import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.DownloadItem @@ -24,16 +23,18 @@ import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView import com.squareup.picasso.Picasso -class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) { +class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : PagingDataAdapter(DIFF_CALLBACK) { private val onItemClickListener: OnItemClickListener private val activity: Activity - private val checkedItems: ArrayList + val checkedItems: ArrayList + var inverted: Boolean private val sharedPreferences: SharedPreferences init { checkedItems = ArrayList() this.onItemClickListener = onItemClickListener this.activity = activity + this.inverted = false sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) } @@ -53,13 +54,16 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = getItem(position) val card = holder.cardView + card.isVisible = item != null + if (item == null) return + card.tag = item.id.toString() val uiHandler = Handler(Looper.getMainLooper()) val thumbnail = card.findViewById(R.id.downloads_image_view) // THUMBNAIL ---------------------------------- if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue")){ - val imageURL = item!!.thumb + val imageURL = item.thumb if (imageURL.isNotEmpty()) { uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) } } else { @@ -127,7 +131,7 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: actionButton.setOnClickListener { onItemClickListener.onActionButtonClick(item.id) } - if (checkedItems.contains(item.id)) { + if ((checkedItems.contains(item.id) && !inverted) || (!checkedItems.contains(item.id) && inverted)) { card.isChecked = true card.strokeWidth = 5 } else { @@ -135,67 +139,69 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: card.strokeWidth = 0 } card.setOnClickListener { - if (checkedItems.size > 0) { - checkCard(card, item.id) + if (checkedItems.size > 0 || inverted) { + checkCard(card, item.id, position) } else { onItemClickListener.onCardClick(item.id) } } card.setOnLongClickListener { - checkCard(card, item.id) + checkCard(card, item.id, position) true } } @SuppressLint("NotifyDataSetChanged") - fun clearCheckeditems() { - for (i in 0 until itemCount){ - val item = getItem(i) - if (checkedItems.find { it == item?.id } != null){ - checkedItems.remove(item?.id) - notifyItemChanged(i) - } - } - + fun clearCheckedItems() { + inverted = false checkedItems.clear() - } - - @SuppressLint("NotifyDataSetChanged") - fun checkAll(items: List?){ - checkedItems.clear() - checkedItems.addAll(items!!.map { it!!.id }) notifyDataSetChanged() } @SuppressLint("NotifyDataSetChanged") - fun invertSelected(items: List?){ - val invertedList = mutableListOf() - items?.forEach { - if (!checkedItems.contains(it!!.id)) invertedList.add(it.id) - } + fun checkAll() { checkedItems.clear() - checkedItems.addAll(invertedList) + inverted = true notifyDataSetChanged() } - private fun checkCard(card: MaterialCardView, itemID: Long) { + @SuppressLint("NotifyDataSetChanged") + fun invertSelected() { + inverted = !inverted + notifyDataSetChanged() + } + + fun getSelectedObjectsCount(totalSize: Int) : Int{ + return if (inverted){ + totalSize - checkedItems.size + }else{ + checkedItems.size + } + } + + + + private fun checkCard(card: MaterialCardView, itemID: Long, position: Int) { if (card.isChecked) { card.strokeWidth = 0 - checkedItems.remove(itemID) + if (inverted) checkedItems.add(itemID) + else checkedItems.remove(itemID) } else { card.strokeWidth = 5 - checkedItems.add(itemID) + if (inverted) checkedItems.remove(itemID) + else checkedItems.add(itemID) } + card.isChecked = !card.isChecked - onItemClickListener.onCardSelect(itemID, card.isChecked) + onItemClickListener.onCardSelect(card.isChecked, position) } interface OnItemClickListener { fun onActionButtonClick(itemID: Long) fun onCardClick(itemID: Long) - fun onCardSelect(itemID: Long, isChecked: Boolean) + fun onCardSelect(isChecked: Boolean, position: Int) } companion object { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/PlaylistAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/PlaylistAdapter.kt index b676fd0f..76719fce 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/PlaylistAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/PlaylistAdapter.kt @@ -21,6 +21,7 @@ import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.ResultItem import com.squareup.picasso.Picasso +import org.w3c.dom.Text import java.util.* @@ -70,17 +71,9 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) } } - // TITLE ---------------------------------- - val itemTitle = card.findViewById(R.id.title) - var title = item!!.title - if (title.length > 100) { - title = title.substring(0, 40) + "..." - } - itemTitle.text = title - - - val duration = card.findViewById(R.id.duration) - duration.text = item.duration + card.findViewById(R.id.title).text = item!!.title + card.findViewById(R.id.author).text = item.author + card.findViewById(R.id.duration).text = item.duration // CHECKBOX ---------------------------------- val check = card.findViewById(R.id.checkBox) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/Converters.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/Converters.kt index f4850317..5537c399 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/Converters.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/Converters.kt @@ -13,16 +13,9 @@ import java.lang.reflect.Type class Converters { @TypeConverter - fun stringToListOfFormats(value: String?): ArrayList { - val listType: Type = object : TypeToken?>() {}.type - return Gson().fromJson(value, listType) - } - + fun stringToListOfFormats(value: String) = Gson().fromJson(value, Array::class.java).toMutableList() @TypeConverter - fun listOfFormatsToString(list: ArrayList?): String { - val gson = Gson() - return gson.toJson(list) - } + fun listOfFormatsToString(list: List?) = Gson().toJson(list).toString() @TypeConverter fun formatToString(format: Format): String = Gson().toJson(format) @@ -53,13 +46,10 @@ class Converters { fun stringToVideoPreferences(string: String): VideoPreferences = Gson().fromJson(string, VideoPreferences::class.java) @TypeConverter - fun stringToListOfChapters(value: String?): ArrayList? { - val listType: Type = object : TypeToken?>() {}.type - return Gson().fromJson(value, listType) - } + fun stringToListOfChapters(value: String?) = Gson().fromJson(value, Array::class.java).toMutableList() @TypeConverter - fun listOfChaptersToString(list: ArrayList?): String { + fun listOfChaptersToString(list: List?): String { val gson = Gson() return gson.toJson(list) } 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 ac349fc0..70357a75 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt @@ -1,9 +1,14 @@ package com.deniscerri.ytdlnis.database import android.content.Context +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource import androidx.room.* import com.deniscerri.ytdlnis.database.dao.* import com.deniscerri.ytdlnis.database.models.* +import kotlinx.coroutines.flow.Flow @TypeConverters(Converters::class) @Database( 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 15550964..c8fdeb4f 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 @@ -1,20 +1,26 @@ 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.Format +import com.deniscerri.ytdlnis.database.repository.DownloadRepository import kotlinx.coroutines.flow.Flow @Dao interface DownloadDao { @Query("SELECT * FROM downloads ORDER BY status") - fun getAllDownloads() : Flow> + fun getAllDownloads() : PagingSource @Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'") fun getActiveDownloads() : Flow> @Query("SELECT COUNT(*) FROM downloads WHERE status='Active' or status='Paused'") - fun getActiveDownloadsCount() : Flow + fun getActiveDownloadsCountFlow() : Flow + + @Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)") + fun getDownloadsCountByStatus(status : List) : Flow @Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'") fun getActiveDownloadsList() : List @@ -22,26 +28,41 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'") fun getActiveAndQueuedDownloadsList() : List + @Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'") + fun getActiveAndQueuedDownloads() : Flow> + @Query("SELECT * FROM downloads WHERE status='Queued' or status='QueuedPaused' ORDER BY downloadStartTime, id") - fun getQueuedDownloads() : Flow> + fun getQueuedDownloads() : PagingSource + + @Query("SELECT format FROM downloads WHERE status='Queued' or status='QueuedPaused'") + fun getSelectedFormatFromQueued() : List + + @Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status='Queued' ORDER BY downloadStartTime, id LIMIT 20") + fun getQueuedDownloadsThatAreNotScheduledChunked(currentTime: Long) : Flow> + + @Query("SELECT * FROM downloads WHERE status='Queued' or status='QueuedPaused' or status='Paused' ORDER BY downloadStartTime, id") + fun getQueuedAndPausedDownloads() : Flow> @Query("SELECT * FROM downloads WHERE status='Queued' or status='QueuedPaused' ORDER BY downloadStartTime, id") fun getQueuedDownloadsList() : List @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") - fun getCancelledDownloads() : Flow> + fun getCancelledDownloads() : PagingSource @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") fun getCancelledDownloadsList() : List + @Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'") + fun getPausedDownloadsList() : List + @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") - fun getErroredDownloads() : Flow> + fun getErroredDownloads() : PagingSource @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") fun getErroredDownloadsList() : List @Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC") - fun getSavedDownloads() : Flow> + fun getSavedDownloads() : PagingSource @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") fun getDownloadById(id: Long) : DownloadItem @@ -52,6 +73,22 @@ interface DownloadDao { @Query("SELECT status FROM downloads WHERE id=:id") fun checkStatus(id: Long) : String + @Query("UPDATE downloads " + + "SET status = CASE " + + " WHEN status = 'Active' THEN 'Paused' " + + " WHEN status = 'Queued' THEN 'QueuedPaused' " + + " ELSE status " + + " END;") + fun pauseActiveAndQueued() + + @Query("UPDATE downloads " + + "SET status = CASE " + + " WHEN status = 'Paused' THEN 'Active' " + + " WHEN status = 'QueuedPaused' THEN 'Queued' " + + " ELSE status " + + " END;") + fun unPauseActiveAndQueued() + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(item: DownloadItem) : Long @@ -73,7 +110,7 @@ interface DownloadDao { @Query("DELETE FROM downloads WHERE status='Saved'") suspend fun deleteSaved() - @Query("UPDATE downloads SET status='Cancelled' WHERE status='Queued'") + @Query("UPDATE downloads SET status='Cancelled' WHERE status='Queued' or status='QueuedPaused'") suspend fun cancelQueued() @Query("DELETE FROM downloads WHERE status='Processing' AND id=:id") @@ -82,6 +119,9 @@ interface DownloadDao { @Upsert suspend fun update(item: DownloadItem) + @Upsert + fun updateMultiple(items :List) + @Query("SELECT * FROM downloads ORDER BY id DESC LIMIT 1") fun getLatest() : DownloadItem @@ -94,4 +134,38 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE url=:url AND format=:format AND (status='Error' OR status='Cancelled') LIMIT 1") fun getUnfinishedByURLAndFormat(url: String, format: String) : DownloadItem + + + @Query("SELECT COUNT(downloadStartTime) = 0 from downloads WHERE id in (:items) AND " + + "CASE :inverted " + + "WHEN :inverted = 'false' THEN downloadStartTime > :currentStartTime " + + "WHEN :inverted = 'true' THEN downloadStartTime < :currentStartTime ELSE downloadStartTime < -1 END") + fun checkAllQueuedItemsAreScheduledAfterNow(items: List, inverted: String, currentStartTime: Long) : Boolean + + + @Query("Select id from downloads where id not in (:list) and status in (:status)") + fun getDownloadIDsNotPresentInList(list: List, status: List) : List + + @Query("UPDATE downloads SET downloadStartTime=0 where id in (:list)") + fun resetScheduleTimeForItems(list: List) + + @Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)") + fun reQueueDownloadItems(list: List) + + @Transaction + fun putAtTopOfTheQueue(id: Long){ + val downloads = getQueuedDownloadsList() + val newID = downloads.first().id + + updateDownloadID(id, -id) + resetScheduleTimeForItems(listOf(-id)) + + downloads.reversed().dropWhile { it.id == id }.forEach { + updateDownloadID(it.id, it.id + 1) + } + updateDownloadID(-id, newID) + } + + @Query("Update downloads set id=:newId where id=:id") + fun updateDownloadID(id: Long, newId: Long) } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/SearchHistoryDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/SearchHistoryDao.kt index e0e38f8a..94bd2056 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/SearchHistoryDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/SearchHistoryDao.kt @@ -12,7 +12,7 @@ interface SearchHistoryDao { fun getAllByKeyword(keyword: String) : List @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insert(new: SearchHistoryItem) + suspend fun insert(newItem: SearchHistoryItem) @Query("DELETE FROM searchHistory") suspend fun deleteAll() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt index 20e574d4..bc34fd41 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt @@ -20,7 +20,7 @@ data class DownloadItem( var container: String, @ColumnInfo(defaultValue = "") var downloadSections: String, - val allFormats: ArrayList, + val allFormats: MutableList, var downloadPath: String, var website: String, val downloadSize: String, diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/ResultItem.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/ResultItem.kt index 8c44e532..4da30423 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/ResultItem.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/ResultItem.kt @@ -15,9 +15,9 @@ data class ResultItem( val thumb: String, val website: String, var playlistTitle: String, - var formats: ArrayList, + var formats: MutableList, @ColumnInfo(defaultValue = "") var urls: String, - var chapters: ArrayList?, + var chapters: MutableList?, var creationTime: Long = System.currentTimeMillis() / 1000, ) \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt index 87c695c7..206acc30 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt @@ -1,19 +1,36 @@ package com.deniscerri.ytdlnis.database.repository -import androidx.lifecycle.LiveData -import com.deniscerri.ytdlnis.database.Converters +import androidx.paging.Pager +import androidx.paging.PagingConfig import com.deniscerri.ytdlnis.database.dao.DownloadDao import com.deniscerri.ytdlnis.database.models.DownloadItem import kotlinx.coroutines.flow.Flow + class DownloadRepository(private val downloadDao: DownloadDao) { - val allDownloads : Flow> = downloadDao.getAllDownloads() + val allDownloads : Pager = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), + pagingSourceFactory = {downloadDao.getAllDownloads()} + ) val activeDownloads : Flow> = downloadDao.getActiveDownloads() - val activeDownloadsCount : Flow = downloadDao.getActiveDownloadsCount() - val queuedDownloads : Flow> = downloadDao.getQueuedDownloads() - val cancelledDownloads : Flow> = downloadDao.getCancelledDownloads() - val erroredDownloads : Flow> = downloadDao.getErroredDownloads() - val savedDownloads : Flow> = downloadDao.getSavedDownloads() + val queuedDownloads : Pager = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), + pagingSourceFactory = {downloadDao.getQueuedDownloads()} + ) + val cancelledDownloads : Pager = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), + pagingSourceFactory = {downloadDao.getCancelledDownloads()} + ) + val erroredDownloads : Pager = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), + pagingSourceFactory = {downloadDao.getErroredDownloads()} + ) + val savedDownloads : Pager = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), + pagingSourceFactory = {downloadDao.getSavedDownloads()} + ) + + val activeDownloadsCount : Flow = downloadDao.getActiveDownloadsCountFlow() enum class Status { Active, Paused, Queued, QueuedPaused, Error, Cancelled, Saved @@ -27,8 +44,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) { return downloadDao.insertAll(items) } - suspend fun delete(item: DownloadItem){ - downloadDao.delete(item.id) + suspend fun delete(id: Long){ + downloadDao.delete(id) } suspend fun update(item: DownloadItem){ @@ -61,6 +78,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) { return downloadDao.getCancelledDownloadsList() } + fun getPausedDownloads() : List { + return downloadDao.getPausedDownloadsList() + } + fun getErroredDownloads() : List { return downloadDao.getErroredDownloadsList() } @@ -81,20 +102,12 @@ class DownloadRepository(private val downloadDao: DownloadDao) { downloadDao.cancelQueued() } - fun getLastDownloadId() : Long { - return downloadDao.getLastDownloadId() + fun pauseDownloads(){ + downloadDao.pauseActiveAndQueued() } - - fun checkIfReDownloadingErroredOrCancelled(item: DownloadItem) : Long { - val converters = Converters() - val format = converters.formatToString(item.format) - return try { - val i = downloadDao.getUnfinishedByURLAndFormat(item.url, format) - i.id - }catch (e: Exception){ - 0L - } + fun unPauseDownloads(){ + downloadDao.unPauseActiveAndQueued() } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt index 4d83af70..7e4b7cc8 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt @@ -12,23 +12,24 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope -import com.anggrayudi.storage.file.dataDirectory +import com.deniscerri.ytdlnis.BuildConfig import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.repository.CookieRepository import com.deniscerri.ytdlnis.ui.more.WebViewActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json import java.io.File +import java.util.Date class CookieViewModel(private val application: Application) : AndroidViewModel(application) { private val repository: CookieRepository val items: LiveData> - private val jsonFormat = Json { prettyPrint = true } - private val cookieHeader = "# Netscape HTTP Cookie File\n" + - "# WebView Generated by the YTDLnis app\n" + private val cookieHeader = + "# Netscape HTTP Cookie File\n" + + "# WebView Generated by the YTDLnis app\n" + + "# This is a generated file! Do not edit." init { val dao = DBManager.getInstance(application).cookieDao @@ -72,8 +73,7 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a if (!hasCookies()) throw Exception("There is no cookies in the database!") flush() } - - val dbPath = File("/data/data/com.deniscerri.ytdl/").walkTopDown().find { it.name == "Cookies" } + val dbPath = File("/data/data/${BuildConfig.APPLICATION_ID}/").walkTopDown().find { it.name == "Cookies" } ?: throw Exception("Cookies File not found!") SQLiteDatabase.openDatabase( @@ -139,11 +139,12 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager var clip = clipboard.primaryClip!!.getItemAt(0).text Log.e("Aaa", clip.toString()) - if (clip.startsWith(cookieHeader)){ + + if (clip.startsWith("# Netscape HTTP Cookie File")){ clip = clip.removePrefix(cookieHeader) val cookie = CookieItem( 0, - "Cookie Import [${System.currentTimeMillis()}]", + "Cookie Import at [${Date()}]", clip.toString() ) insert(cookie) 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 2145255e..66d54ad1 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 @@ -8,22 +8,20 @@ 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.OneTimeWorkRequest import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkInfo import androidx.work.WorkManager -import androidx.work.WorkRequest -import androidx.work.workDataOf +import androidx.work.await import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.DBManager @@ -44,18 +42,12 @@ import com.deniscerri.ytdlnis.work.DownloadWorker import com.google.gson.Gson import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext import java.io.File import java.util.Locale -import java.util.UUID import java.util.concurrent.TimeUnit -import kotlin.random.Random -import kotlin.random.nextInt class DownloadViewModel(application: Application) : AndroidViewModel(application) { @@ -64,13 +56,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application private val sharedPreferences: SharedPreferences private val commandTemplateDao: CommandTemplateDao private val infoUtil : InfoUtil - val allDownloads : LiveData> - val queuedDownloads : LiveData> + val allDownloads : Flow> + val queuedDownloads : Flow> val activeDownloads : LiveData> val activeDownloadsCount : LiveData - val cancelledDownloads : LiveData> - val erroredDownloads : LiveData> - val savedDownloads : LiveData> + val cancelledDownloads : Flow> + val erroredDownloads : Flow> + val savedDownloads : Flow> private var bestVideoFormat : Format private var bestAudioFormat : Format @@ -81,6 +73,11 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application private val audioFormatIDPreference: List private val resources : Resources private var extraCommands: String = "" + + private var audioContainer: String? + private var videoContainer: String? + private var videoCodec: String? + enum class Type { audio, video, command } @@ -93,13 +90,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application commandTemplateDao = DBManager.getInstance(application).commandTemplateDao infoUtil = InfoUtil(application) - allDownloads = repository.allDownloads.asLiveData() - queuedDownloads = repository.queuedDownloads.asLiveData() + allDownloads = repository.allDownloads.flow + queuedDownloads = repository.queuedDownloads.flow activeDownloads = repository.activeDownloads.asLiveData() activeDownloadsCount = repository.activeDownloadsCount.asLiveData() - savedDownloads = repository.savedDownloads.asLiveData() - cancelledDownloads = repository.cancelledDownloads.asLiveData() - erroredDownloads = repository.erroredDownloads.asLiveData() + savedDownloads = repository.savedDownloads.flow + cancelledDownloads = repository.cancelledDownloads.flow + erroredDownloads = repository.erroredDownloads.flow viewModelScope.launch(Dispatchers.IO){ if (sharedPreferences.getBoolean("use_extra_commands", false)){ extraCommands = commandTemplateDao.getAllTemplatesAsExtraCommands().joinToString(" ") @@ -107,8 +104,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString() - formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",") - audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",") + formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() } + audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() } val confTmp = Configuration(application.resources.configuration) confTmp.locale = Locale(sharedPreferences.getString("app_language", "en")!!) @@ -117,7 +114,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val videoFormatValues = resources.getStringArray(R.array.video_formats_values) - val videoContainer = sharedPreferences.getString("video_format", "Default") + videoContainer = sharedPreferences.getString("video_format", "Default") defaultVideoFormats = mutableListOf() videoFormatValues.forEach { @@ -132,29 +129,55 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application ) defaultVideoFormats.add(tmp) } + formatIDPreference.reversed().forEach { + val tmp = Format( + it, + videoContainer!!, + "", + "", + "", + 0, + it + ) + defaultVideoFormats.add(tmp) + } bestVideoFormat = defaultVideoFormats.last() - val audioContainer = sharedPreferences.getString("audio_format", "mp3") - bestAudioFormat = Format( - "best", - audioContainer!!, - "", - "", - "", - 0, - "best" - ) + audioContainer = sharedPreferences.getString("audio_format", "mp3") + bestAudioFormat = if (audioFormatIDPreference.isEmpty()){ + Format( + "best", + audioContainer!!, + "", + "", + "", + 0, + "best" + ) + }else{ + Format( + audioFormatIDPreference.first(), + audioContainer!!, + "", + "", + "", + 0, + audioFormatIDPreference.first() + ) + } + + videoCodec = sharedPreferences.getString("video_codec", "") } - fun deleteDownload(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO) { - repository.delete(item) + fun deleteDownload(id: Long) = viewModelScope.launch(Dispatchers.IO) { + repository.delete(id) } suspend fun updateDownload(item: DownloadItem){ if (sharedPreferences.getBoolean("incognito", false)){ if (item.status == DownloadRepository.Status.Cancelled.toString() || item.status == DownloadRepository.Status.Error.toString()){ - repository.delete(item) + repository.delete(item.id) } }else{ repository.update(item) @@ -345,7 +368,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application return cloneFormat ( try { try{ - formats.first { it.format_note.contains("audio", ignoreCase = true) && audioFormatIDPreference.contains(it.format_id)} + formats.filter { it.format_note.contains("audio", ignoreCase = true)}.first { + audioFormatIDPreference.contains(it.format_id) || + it.container == audioContainer + } }catch (e: Exception){ formats.last { it.format_note.contains("audio", ignoreCase = true) } } @@ -358,24 +384,19 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application Type.video -> { return cloneFormat( try { - val theFormats = formats.ifEmpty { defaultVideoFormats } - try { - formats.first { !it.format_note.contains("audio", ignoreCase = true) && formatIDPreference.contains(it.format_id)} - }catch (e: Exception){ - when (videoQualityPreference) { - "worst" -> { - theFormats.first {!it.format_note.contains("audio", ignoreCase = true) } - } - "best" -> { - theFormats.last {!it.format_note.contains("audio", ignoreCase = true) } - } - else -> { - try{ - theFormats.last {it.format_note.contains(videoQualityPreference.substring(0, videoQualityPreference.length - 1)) } - }catch (e: Exception){ - theFormats.last { !it.format_note.contains("audio", ignoreCase = true) } - } - } + val theFormats = formats.filter { !it.format_note.contains("audio", true) }.ifEmpty { defaultVideoFormats } + when (videoQualityPreference) { + "worst" -> { + theFormats.first() + } + else /*best*/ -> { + val requirements: MutableList<(Format) -> Boolean> = mutableListOf() + 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.substring(0, videoQualityPreference.length - 1)) } + requirements.add { it: Format -> it.vcodec.startsWith(videoCodec!!, true) } + + theFormats.reversed().maxByOrNull { f -> requirements.count{ req -> req(f)} } ?: throw Exception() } } }catch (e: Exception){ @@ -406,6 +427,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val audioFormats = resources.getStringArray(R.array.audio_formats) val formats = mutableListOf() val containerPreference = sharedPreferences.getString("audio_format", "") + audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) } audioFormats.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) } return formats } @@ -414,6 +436,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val videoFormats = resources.getStringArray(R.array.video_formats_values) val formats = mutableListOf() val containerPreference = sharedPreferences.getString("video_format", "") + formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"Default","", "",0, it)) } videoFormats.forEach { formats.add(Format(it, containerPreference!!,"Default","", "",0, it)) } return formats } @@ -436,6 +459,14 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application repository.insert(item) } + fun pauseDownloads() = viewModelScope.launch(Dispatchers.IO){ + repository.pauseDownloads() + } + + fun unPauseDownloads() = viewModelScope.launch(Dispatchers.IO){ + repository.unPauseDownloads() + } + fun insertAll(items: List)= viewModelScope.launch(Dispatchers.IO){ items.forEach{ repository.insert(it) @@ -466,6 +497,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application return repository.getCancelledDownloads() } + fun getPausedDownloads() : List { + return repository.getPausedDownloads() + } + fun getErrored() : List { return repository.getErroredDownloads() } @@ -483,10 +518,24 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application return repository.getActiveAndQueuedDownloads() } + suspend fun resetScheduleTimeForItemsAndStartDownload(items: List) = CoroutineScope(Dispatchers.IO).launch { + dbManager.downloadDao.resetScheduleTimeForItems(items) + startDownloadWorker(emptyList()) + } + + suspend fun putAtTopOfQueue(id: Long) = CoroutineScope(Dispatchers.IO).launch{ + dbManager.downloadDao.putAtTopOfTheQueue(id) + startDownloadWorker(emptyList()) + } + + suspend fun reQueueDownloadItems(items: List) = CoroutineScope(Dispatchers.IO).launch { + dbManager.downloadDao.reQueueDownloadItems(items) + startDownloadWorker(emptyList()) + } + suspend fun queueDownloads(items: List) = CoroutineScope(Dispatchers.IO).launch { val context = App.instance val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads() - val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true) val queuedItems = mutableListOf() var exists = false @@ -518,33 +567,50 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } } + if (queuedItems.isNotEmpty()){ + startDownloadWorker(queuedItems) + } + } + + private suspend fun startDownloadWorker(queuedItems: List) { + val context = App.instance + val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true) val workManager = WorkManager.getInstance(context) - queuedItems.forEach { + val currentWork = workManager.getWorkInfosByTag("download").await() + if (currentWork.size == 0 || currentWork.none{ it.state == WorkInfo.State.RUNNING } || (queuedItems.isNotEmpty() && queuedItems[0].downloadStartTime != 0L)){ + val currentTime = System.currentTimeMillis() - var delay = if (it.downloadStartTime != 0L){ - it.downloadStartTime - currentTime - } else 0 - if (delay < 0L) delay = 1L + var delay = 1000L + if (queuedItems.isNotEmpty()){ + delay = if (queuedItems[0].downloadStartTime != 0L){ + queuedItems[0].downloadStartTime.minus(currentTime) + } else 0 + if (delay <= 0L) delay = 1000L + } + val workConstraints = Constraints.Builder() if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED) val workRequest = OneTimeWorkRequestBuilder() - .setInputData(Data.Builder().putLong("id", it.id).build()) - .addTag(it.id.toString()) .addTag("download") .setConstraints(workConstraints.build()) - .setInitialDelay(delay, TimeUnit.SECONDS) - .build() + .setInitialDelay(delay, TimeUnit.MILLISECONDS) + + if (delay > 1000L){ + queuedItems.forEach { + workRequest.addTag(it.id.toString()) + } + } workManager.enqueueUniqueWork( - it.id.toString(), + System.currentTimeMillis().toString(), ExistingWorkPolicy.REPLACE, - workRequest + workRequest.build() ) - } + } val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered if (!allowMeteredNetworks && isCurrentNetworkMetered){ @@ -553,12 +619,29 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } } - items.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach { + queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }?.forEach { try{ updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao) }catch (ignored: Exception){} } } + + fun getQueuedCollectedFileSize() : Long { + return dbManager.downloadDao.getSelectedFormatFromQueued().sumOf { it.filesize } + } + + fun getTotalSize(status: List) : LiveData { + return dbManager.downloadDao.getDownloadsCountByStatus(status.map { it.toString() }).asLiveData() + } + + fun checkAllQueuedItemsAreScheduledAfterNow(items: List, inverted: Boolean, currentStartTime: Long) : Boolean { + return dbManager.downloadDao.checkAllQueuedItemsAreScheduledAfterNow(items, inverted.toString(), currentStartTime) + } + + fun getItemIDsNotPresentIn(items: List, status: List) : List { + return dbManager.downloadDao.getDownloadIDsNotPresentInList(items.ifEmpty { listOf(-1L) }, status.map { it.toString() }) + } + private fun updateDownloadItem( downloadItem: DownloadItem, infoUtil: InfoUtil, 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 0088aca4..9b0b2e08 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 @@ -28,10 +28,13 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) val items : LiveData> val loadingItems = MutableLiveData() private val sharedPreferences: SharedPreferences + var state: ResultsState = ResultsState.IDLE + enum class ResultsState { + PROCESSING, IDLE + } init { val dao = DBManager.getInstance(application).resultDao - val commandDao = DBManager.getInstance(application).commandTemplateDao repository = ResultRepository(dao, getApplication().applicationContext) searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao) items = repository.allResults.asLiveData() @@ -61,6 +64,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) } } suspend fun parseQueries(inputQueries: List){ + state = ResultsState.PROCESSING if (inputQueries.size == 1){ parseQuery(inputQueries[0], true) }else { @@ -69,6 +73,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) inputQueries.forEach { parseQuery(it, false) } + state = ResultsState.IDLE loadingItems.postValue(false) } } @@ -97,7 +102,10 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) } catch (e: Exception) { Log.e(tag, e.toString()) } - if (resetResults) loadingItems.postValue(false) + if (resetResults) { + state = ResultsState.IDLE + loadingItems.postValue(false) + } res } } @@ -107,7 +115,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) val m = p.matcher(inputQuery) if (m.find()) { type = "YT_Video" - if (inputQuery.contains("playlist?list=")) { + if (inputQuery.contains("playlist?list=") && !inputQuery.contains("music.youtu")) { type = "YT_Playlist" } } else if (inputQuery.contains("http")) { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelDownloadNotificationReceiver.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelDownloadNotificationReceiver.kt index 5dbe0619..8cf273bc 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelDownloadNotificationReceiver.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelDownloadNotificationReceiver.kt @@ -20,10 +20,8 @@ class CancelDownloadNotificationReceiver : BroadcastReceiver() { if (message != null) { runCatching { val notificationUtil = NotificationUtil(c) - WorkManager.getInstance(c).cancelUniqueWork(id.toString()) - notificationUtil.cancelDownloadNotification(id) YoutubeDL.getInstance().destroyProcessById(id.toString()) - + notificationUtil.cancelDownloadNotification(id) val dbManager = DBManager.getInstance(c) CoroutineScope(Dispatchers.IO).launch{ runCatching { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/PauseDownloadNotificationReceiver.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/PauseDownloadNotificationReceiver.kt index bc816fd5..b1fb0ecc 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/PauseDownloadNotificationReceiver.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/PauseDownloadNotificationReceiver.kt @@ -3,8 +3,13 @@ package com.deniscerri.ytdlnis.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import com.deniscerri.ytdlnis.database.DBManager +import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.util.NotificationUtil import com.yausername.youtubedl_android.YoutubeDL +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class PauseDownloadNotificationReceiver : BroadcastReceiver() { override fun onReceive(c: Context, intent: Intent) { @@ -16,6 +21,14 @@ class PauseDownloadNotificationReceiver : BroadcastReceiver() { notificationUtil.cancelDownloadNotification(id) YoutubeDL.getInstance().destroyProcessById(id.toString()) notificationUtil.createResumeDownload(id, title, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID) + val dbManager = DBManager.getInstance(c) + CoroutineScope(Dispatchers.IO).launch{ + runCatching { + val item = dbManager.downloadDao.getDownloadById(id.toLong()) + item.status = DownloadRepository.Status.Paused.toString() + dbManager.downloadDao.update(item) + } + } } } } 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 8255ff22..bbca7fe9 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt @@ -32,7 +32,7 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.ui.BaseActivity import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog -import com.deniscerri.ytdlnis.ui.downloadcard.SelectPlaylistItemsBottomSheetDialog +import com.deniscerri.ytdlnis.ui.downloadcard.SelectPlaylistItemsDialog import com.deniscerri.ytdlnis.util.ThemeUtil import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -95,10 +95,9 @@ class ShareActivity : BaseActivity() { askPermissions() val action = intent.action - val type = intent.clipData Log.e("aa", intent.toString()) - if (Intent.ACTION_SEND == action && type != null) { - if (intent.getStringExtra(Intent.EXTRA_TEXT) == null){ + if (Intent.ACTION_SEND == action || Intent.ACTION_VIEW == action) { + if (intent.getStringExtra(Intent.EXTRA_TEXT) == null && Intent.ACTION_SEND == action){ intent.setClass(this, MainActivity::class.java) startActivity(intent) finishAffinity() @@ -107,7 +106,7 @@ class ShareActivity : BaseActivity() { runCatching { supportFragmentManager.popBackStack() } - quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false)) + 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) @@ -118,7 +117,11 @@ class ShareActivity : BaseActivity() { this.finish() } - val inputQuery = intent.getStringExtra(Intent.EXTRA_TEXT)!! + val inputQuery = when(action){ + Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)!! + else -> intent.dataString!! + } + lifecycleScope.launch { try { val result = resultViewModel.getItemByURL(inputQuery) @@ -166,7 +169,7 @@ class ShareActivity : BaseActivity() { private fun showSelectPlaylistItems(it: List){ if (sharedPreferences.getBoolean("download_card", true)){ - val bottomSheet = SelectPlaylistItemsBottomSheetDialog(it, DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!)) + val bottomSheet = SelectPlaylistItemsDialog(it, DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!)) bottomSheet.show(supportFragmentManager, "downloadPlaylistSheet") }else{ lifecycleScope.launch(Dispatchers.IO){ 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 512b4c38..37ec610f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt @@ -6,6 +6,8 @@ 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 @@ -40,6 +42,8 @@ import com.deniscerri.ytdlnis.ui.downloadcard.DownloadMultipleBottomSheetDialog import com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.ThemeUtil +import com.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 @@ -55,6 +59,7 @@ 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.* @@ -146,6 +151,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene recyclerView = view.findViewById(R.id.recyclerViewHome) recyclerView?.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) recyclerView?.adapter = homeAdapter + recyclerView?.enableFastScroll() resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel.items.observe(viewLifecycleOwner) { @@ -251,12 +257,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene override fun onResume() { super.onResume() if(arguments?.getString("url") == null){ - resultViewModel.checkTrending() + if (resultViewModel.state == ResultViewModel.ResultsState.IDLE){ + resultViewModel.checkTrending() + }else{ + resultViewModel.loadingItems.postValue(true) + } }else{ - arguments arguments?.remove("url") - arguments?.remove("showDownloadsWithUpdatedFormats") } + arguments?.remove("showDownloadsWithUpdatedFormats") if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){ lifecycleScope.launch { updateSearchViewItems(searchView?.editText?.text, linkYouCopied) @@ -533,7 +542,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene resultViewModel.deleteAll() lifecycleScope.launch(Dispatchers.IO){ Thread.sleep(300) - if(sharedPreferences!!.getBoolean("quick_download", false)){ + 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)) { 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 41ee3576..eb72b99f 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 @@ -37,7 +37,7 @@ import java.util.* import java.util.regex.Pattern -class AddExtraCommandsDialog(private val item: DownloadItem, private val callback: ExtraCommandsListener) : BottomSheetDialogFragment() { +class AddExtraCommandsDialog(private val item: DownloadItem?, private val callback: ExtraCommandsListener) : BottomSheetDialogFragment() { private lateinit var infoUtil: InfoUtil private lateinit var sharedPreferences: SharedPreferences private lateinit var commandTemplateViewModel: CommandTemplateViewModel @@ -86,9 +86,14 @@ class AddExtraCommandsDialog(private val item: DownloadItem, private val callbac val text = view.findViewById(R.id.command) val templates = view.findViewById