From df2dee4c8388b9cde8448c91c89bc3179207ce30 Mon Sep 17 00:00:00 2001 From: deniscerri <64997243+deniscerri@users.noreply.github.com> Date: Sat, 26 Aug 2023 20:48:24 +0200 Subject: [PATCH] stuff - added saved downloads in the settings backup - made icon bigger in downloading notification - fixed notification cancel and pausing not working - fixed app not working in preferred home screen for downloads and more - removed 000001 if the user has made only one cut - removed log id from downloads if the user deleted the log - if the user had multiple preferred audio formats the app would choose the last one and not the first - added ability to combine preferred audio formats by writing like 140+251,250. If the download item finds both first audio formats it will merge them together, otherwise get one of them that is available - fixed app file transfer not working. (had to revert to hidden cache folder as downloads folder wasnt good for some phones) - fixed app saving as mkv even though mp4 is set as container - added shortcuts button to the commands tab so you can drop shortcuts to the current command. - added syntax highlighting in commands tab textbox - fixed tiktok videos not saving properly - added ability to combine all possible combinations of preferred video formats, audio formats, codex and container. The app will consider all preferred video ids. For each of them it will consider preferred vcodec and extension. At the same time for each of those elements it will consider all preferred audio commands and/or audio merge. If the format is a generic quality like 1080p or 720p it will also consider that in the long format query. Also the app will add all combos of video + audio only. And all combos of all video + best audio. All video ids alone and best as final fallback. If the user chooses a proper format, the app will not be this descriptive. Preferred video id and audio id combos will only apply on a quick download or best quality generic download --- app/build.gradle | 2 +- .../com/deniscerri/ytdlnis/MainActivity.kt | 16 +- .../ytdlnis/adapter/CookieAdapter.kt | 5 +- .../ytdlnis/adapter/GenericDownloadAdapter.kt | 1 - .../ytdlnis/adapter/TemplatesAdapter.kt | 5 +- .../ytdlnis/database/dao/DownloadDao.kt | 13 +- .../deniscerri/ytdlnis/database/dao/LogDao.kt | 7 + .../database/models/VideoPreferences.kt | 2 +- .../database/repository/DownloadRepository.kt | 10 +- .../database/viewmodel/DownloadViewModel.kt | 45 +++-- .../database/viewmodel/LogViewModel.kt | 8 +- .../CancelDownloadNotificationReceiver.kt | 7 +- .../ytdlnis/receiver/CancelWorkReceiver.kt | 27 +++ .../PauseDownloadNotificationReceiver.kt | 5 +- .../ytdlnis/receiver/ResumeActivity.kt | 9 +- .../com/deniscerri/ytdlnis/ui/HomeFragment.kt | 5 +- .../ui/downloadcard/AddExtraCommandsDialog.kt | 26 +-- .../downloadcard/DownloadCommandFragment.kt | 90 +++++----- .../ui/downloads/ActiveDownloadsFragment.kt | 66 ++++---- .../ytdlnis/ui/more/TerminalActivity.kt | 24 +-- .../more/settings/FolderSettingsFragment.kt | 6 +- .../ui/more/settings/MainSettingsFragment.kt | 37 ++++ .../com/deniscerri/ytdlnis/util/FileUtil.kt | 24 +-- .../com/deniscerri/ytdlnis/util/InfoUtil.kt | 159 +++++++++++++----- .../ytdlnis/util/NotificationUtil.kt | 66 ++++---- .../com/deniscerri/ytdlnis/util/UiUtil.kt | 62 ++++++- .../deniscerri/ytdlnis/work/DownloadWorker.kt | 33 ++-- .../ytdlnis/work/TerminalDownloadWorker.kt | 4 +- .../ytdlnis/work/UpdateYTDLWorker.kt | 3 - app/src/main/res/layout/adjust_command.xml | 18 +- app/src/main/res/layout/adjust_video.xml | 2 +- .../main/res/layout/command_template_item.xml | 117 +++++++------ .../configure_download_bottom_sheet.xml | 2 +- .../main/res/layout/format_details_sheet.xml | 153 +++++++++-------- .../res/layout/fragment_download_command.xml | 28 ++- .../fragment_generic_download_queue.xml | 5 - app/src/main/res/xml/folders_preference.xml | 1 - .../main/res/xml/processing_preferences.xml | 2 +- 38 files changed, 665 insertions(+), 430 deletions(-) create mode 100644 app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelWorkReceiver.kt diff --git a/app/build.gradle b/app/build.gradle index 09936bfe..60d63085 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ def properties = new Properties() def versionMajor = 1 def versionMinor = 6 def versionPatch = 5 -def versionBuild = 0 // bump for dogfood builds, public betas, etc. +def versionBuild = 2 // bump for dogfood builds, public betas, etc. def versionExt = "" if (versionBuild > 0){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt index 1fbea725..40af3bc3 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt @@ -120,14 +120,14 @@ class MainActivity : BaseActivity() { sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) -// val graph = navController.navInflater.inflate(R.navigation.nav_graph) -// graph.setStartDestination(R.id.homeFragment) -// when(sharedPreferences.getString("start_destination", "")) { -// "History" -> graph.setStartDestination(R.id.historyFragment) -// "More" -> if (navigationView is NavigationBarView) graph.setStartDestination(R.id.moreFragment) -// } -// -// navController.graph = graph + val graph = navController.navInflater.inflate(R.navigation.nav_graph) + graph.setStartDestination(R.id.homeFragment) + when(sharedPreferences.getString("start_destination", "")) { + "History" -> graph.setStartDestination(R.id.historyFragment) + "More" -> if (navigationView is NavigationBarView) graph.setStartDestination(R.id.moreFragment) + } + + navController.graph = graph if (navigationView is NavigationBarView){ (navigationView as NavigationBarView).setupWithNavController(navController) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/CookieAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/CookieAdapter.kt index 3e7ec1c5..c7ec1570 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/CookieAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/CookieAdapter.kt @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.CookieItem +import com.google.android.material.card.MaterialCardView class CookieAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) { private val onItemClickListener: OnItemClickListener @@ -23,10 +24,10 @@ class CookieAdapter(onItemClickListener: OnItemClickListener, activity: Activity } class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { - val item: ConstraintLayout + val item: MaterialCardView init { - item = itemView.findViewById(R.id.command_template_item_constraint) + item = itemView.findViewById(R.id.command_card) } } 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 1071d67f..aa59eaad 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/GenericDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/GenericDownloadAdapter.kt @@ -54,7 +54,6 @@ 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() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/TemplatesAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/TemplatesAdapter.kt index 906a8490..2d99590d 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/TemplatesAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/TemplatesAdapter.kt @@ -13,6 +13,7 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.CommandTemplate +import com.google.android.material.card.MaterialCardView class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) { private val onItemClickListener: OnItemClickListener @@ -24,10 +25,10 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ } class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { - val item: ConstraintLayout + val item: MaterialCardView init { - item = itemView.findViewById(R.id.command_template_item_constraint) + item = itemView.findViewById(R.id.command_card) } } 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 c8fdeb4f..fcbcb661 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 @@ -4,7 +4,6 @@ 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 @@ -23,6 +22,9 @@ interface DownloadDao { fun getDownloadsCountByStatus(status : List) : Flow @Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'") + fun getActiveAndPausedDownloadsList() : List + + @Query("SELECT * FROM downloads WHERE status='Active'") fun getActiveDownloadsList() : List @Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'") @@ -64,6 +66,9 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC") fun getSavedDownloads() : PagingSource + @Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC") + fun getSavedDownloadsList() : List + @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") fun getDownloadById(id: Long) : DownloadItem @@ -119,6 +124,12 @@ interface DownloadDao { @Upsert suspend fun update(item: DownloadItem) + @Query("UPDATE downloads SET logID=null") + fun removeAllLogID() + + @Query("UPDATE downloads SET logID=null WHERE logID=:logID") + fun removeLogID(logID: Long) + @Upsert fun updateMultiple(items :List) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/LogDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/LogDao.kt index c5c84ee1..6214918f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/LogDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/LogDao.kt @@ -22,6 +22,13 @@ interface LogDao { @Query("DELETE FROM logs WHERE id=:itemId") suspend fun delete(itemId: Long) + @Transaction + suspend fun deleteItems(list: List){ + list.forEach{ + delete(it.id) + } + } + @Update(onConflict = OnConflictStrategy.REPLACE) suspend fun update(item: LogItem) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/VideoPreferences.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/VideoPreferences.kt index 2dd351ae..3661993a 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/VideoPreferences.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/VideoPreferences.kt @@ -6,7 +6,7 @@ data class VideoPreferences ( var splitByChapters: Boolean = false, var sponsorBlockFilters: ArrayList = arrayListOf(), var writeSubs: Boolean = false, - var subsLanguages: String = "all", + var subsLanguages: String = "en.*,.*-orig", var audioFormatIDs : ArrayList = arrayListOf(), var removeAudio: Boolean = false ) 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 206acc30..d7cbdf2e 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 @@ -63,7 +63,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) { } fun getActiveDownloads() : List { - return downloadDao.getActiveDownloadsList() + return downloadDao.getActiveAndPausedDownloadsList() } fun getActiveAndQueuedDownloads() : List { @@ -110,4 +110,12 @@ class DownloadRepository(private val downloadDao: DownloadDao) { downloadDao.unPauseActiveAndQueued() } + fun removeLogID(logID: Long){ + downloadDao.removeLogID(logID) + } + + fun removeAllLogID(){ + downloadDao.removeAllLogID() + } + } \ No newline at end of file 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 66d54ad1..d76accbc 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 @@ -77,6 +77,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application private var audioContainer: String? private var videoContainer: String? private var videoCodec: String? + private val dao: DownloadDao enum class Type { audio, video, command @@ -84,7 +85,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application init { dbManager = DBManager.getInstance(application) - val dao = dbManager.downloadDao + dao = dbManager.downloadDao repository = DownloadRepository(dao) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) commandTemplateDao = DBManager.getInstance(application).commandTemplateDao @@ -157,13 +158,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application ) }else{ Format( - audioFormatIDPreference.first(), + audioFormatIDPreference.first().split("+").first(), audioContainer!!, "", "", "", 0, - audioFormatIDPreference.first() + audioFormatIDPreference.first().split("+").first() ) } @@ -220,8 +221,24 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val audioPreferences = AudioPreferences(embedThumb, false, ArrayList(sponsorblock!!)) - val hasPreferredAudioFormats = resultItem.formats.filter { audioFormatIDPreference.contains(it.format_id) } - val videoPreferences = VideoPreferences(embedSubs, addChapters, false, ArrayList(sponsorblock), saveSubs, audioFormatIDs = if (hasPreferredAudioFormats.isEmpty()) arrayListOf() else arrayListOf(hasPreferredAudioFormats.map { it.format_id }.first())) + val preferredAudioFormats = arrayListOf() + for (f in resultItem.formats.sortedBy { it.format_id }){ + val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) } + if (fId != null) { + if (fId.split("+").all { resultItem.formats.map { f-> f.format_id }.contains(it) }){ + preferredAudioFormats.addAll(fId.split("+")) + break + } + } + } + + val videoPreferences = VideoPreferences( + embedSubs, + addChapters, false, + ArrayList(sponsorblock), + saveSubs, + audioFormatIDs = preferredAudioFormats + ) return DownloadItem(0, resultItem.url, @@ -368,10 +385,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application return cloneFormat ( try { try{ - formats.filter { it.format_note.contains("audio", ignoreCase = true)}.first { - audioFormatIDPreference.contains(it.format_id) || - it.container == audioContainer - } + formats.filter { it.format_note.contains("audio", ignoreCase = true)}.sortedBy { it.format_id } + .firstOrNull { audioFormatIDPreference.indexOfFirst { a -> a.contains(it.format_id) } >= 0 || it.container == audioContainer} ?: throw Exception() }catch (e: Exception){ formats.last { it.format_note.contains("audio", ignoreCase = true) } } @@ -384,14 +399,14 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application Type.video -> { return cloneFormat( try { - val theFormats = formats.filter { !it.format_note.contains("audio", true) }.ifEmpty { defaultVideoFormats } + val theFormats = formats.filter { !it.format_note.contains("audio", true) }.sortedBy { it.format_id }.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 -> formatIDPreference.sorted().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) } @@ -505,6 +520,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application return repository.getErroredDownloads() } + fun getSaved() : List { + return dao.getSavedDownloadsList() + } + private fun cloneFormat(item: Format) : Format { val string = Gson().toJson(item, Format::class.java) return Gson().fromJson(string, Format::class.java) @@ -567,9 +586,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } } - if (queuedItems.isNotEmpty()){ - startDownloadWorker(queuedItems) - } + startDownloadWorker(queuedItems) } private suspend fun startDownloadWorker(queuedItems: List) { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/LogViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/LogViewModel.kt index 4229e3b2..80f8f09f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/LogViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/LogViewModel.kt @@ -7,17 +7,19 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.models.LogItem +import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.LogRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class LogViewModel(private val application: Application) : AndroidViewModel(application) { private val repository: LogRepository + private val downloadRepository: DownloadRepository val items: LiveData> init { - val dao = DBManager.getInstance(application).logDao - repository = LogRepository(dao) + repository = LogRepository(DBManager.getInstance(application).logDao) + downloadRepository = DownloadRepository(DBManager.getInstance(application).downloadDao) items = repository.items.asLiveData() } @@ -36,10 +38,12 @@ class LogViewModel(private val application: Application) : AndroidViewModel(appl fun delete(item: LogItem) = viewModelScope.launch(Dispatchers.IO) { repository.delete(item) + downloadRepository.removeLogID(item.id) } fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { repository.deleteAll() + downloadRepository.removeAllLogID() } fun update(newLine: String, id: Long) = viewModelScope.launch(Dispatchers.IO) { 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 8cf273bc..a242c19e 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelDownloadNotificationReceiver.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelDownloadNotificationReceiver.kt @@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.util.Log import androidx.work.WorkManager import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.repository.DownloadRepository @@ -15,10 +16,10 @@ import kotlinx.coroutines.runBlocking class CancelDownloadNotificationReceiver : BroadcastReceiver() { override fun onReceive(c: Context, intent: Intent) { - val message = intent.getStringExtra("cancel") - val id = intent.getIntExtra("workID", 0) - if (message != null) { + val id = intent.getIntExtra("itemID", 0) + if (id > 0) { runCatching { + Log.e("aa", id.toString()) val notificationUtil = NotificationUtil(c) YoutubeDL.getInstance().destroyProcessById(id.toString()) notificationUtil.cancelDownloadNotification(id) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelWorkReceiver.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelWorkReceiver.kt new file mode 100644 index 00000000..3632fde5 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelWorkReceiver.kt @@ -0,0 +1,27 @@ +package com.deniscerri.ytdlnis.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.work.WorkManager +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 +import kotlinx.coroutines.runBlocking + +class CancelWorkReceiver : BroadcastReceiver() { + override fun onReceive(c: Context, intent: Intent) { + val id = intent.getStringExtra("workID") + if (!id.isNullOrBlank()) { + runCatching { + WorkManager.getInstance(c).cancelAllWorkByTag(id) + } + + } + } +} \ No newline at end of file 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 b1fb0ecc..54a21825 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/PauseDownloadNotificationReceiver.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/PauseDownloadNotificationReceiver.kt @@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.util.Log import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.util.NotificationUtil @@ -13,14 +14,14 @@ import kotlinx.coroutines.launch class PauseDownloadNotificationReceiver : BroadcastReceiver() { override fun onReceive(c: Context, intent: Intent) { - val id = intent.getIntExtra("workID", 0) + val id = intent.getIntExtra("itemID", 0) if (id != 0) { runCatching { val title = intent.getStringExtra("title") val notificationUtil = NotificationUtil(c) notificationUtil.cancelDownloadNotification(id) YoutubeDL.getInstance().destroyProcessById(id.toString()) - notificationUtil.createResumeDownload(id, title, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID) + notificationUtil.createResumeDownload(id, title) val dbManager = DBManager.getInstance(c) CoroutineScope(Dispatchers.IO).launch{ runCatching { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ResumeActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ResumeActivity.kt index 1492c117..d98ed547 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ResumeActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ResumeActivity.kt @@ -63,7 +63,7 @@ class ResumeActivity : BaseActivity() { } private fun handleIntents(intent: Intent) { - val id = intent.getIntExtra("workID", 0) + val id = intent.getIntExtra("itemID", 0) if (id != 0) { try { val loadingBottomSheet = BottomSheetDialog(this) @@ -71,13 +71,12 @@ class ResumeActivity : BaseActivity() { loadingBottomSheet.setContentView(R.layout.please_wait_bottom_sheet) loadingBottomSheet.setOnShowListener { - NotificationUtil(this).cancelDownloadNotification(NotificationUtil.DOWNLOAD_RESUME_NOTIFICATION_ID) + NotificationUtil(this).cancelDownloadNotification(NotificationUtil.DOWNLOAD_RESUME_NOTIFICATION_ID + id) lifecycleScope.launch { val downloadViewModel = ViewModelProvider(this@ResumeActivity)[DownloadViewModel::class.java] withContext(Dispatchers.IO){ - val downloadItem = downloadViewModel.getItemByID(id.toLong()) - downloadViewModel.queueDownloads(listOf(downloadItem)) - finishAffinity() + downloadViewModel.reQueueDownloadItems(listOf(id.toLong())) + finishAffinity() } } 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 37ec610f..a0239791 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt @@ -23,6 +23,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.children import androidx.core.view.forEach +import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider @@ -427,11 +428,11 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene @SuppressLint("InflateParams") private suspend fun updateSearchViewItems(it: Editable?, linkYouCopied: View?){ + searchSuggestionsLinearLayout!!.visibility = GONE + searchHistoryLinearLayout!!.visibility = GONE searchSuggestionsLinearLayout!!.removeAllViews() searchHistoryLinearLayout!!.removeAllViews() - searchSuggestionsLinearLayout!!.visibility = GONE - searchHistoryLinearLayout!!.visibility = GONE linkYouCopied!!.visibility = GONE if (searchView!!.editText.text.isEmpty()){ 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 eb72b99f..08f2e02a 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 @@ -24,6 +24,7 @@ import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.UiUtil +import com.deniscerri.ytdlnis.util.UiUtil.enableTextHighlight import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -95,29 +96,8 @@ class AddExtraCommandsDialog(private val item: DownloadItem?, private val callba view.findViewById(R.id.current).visibility = View.GONE } - - //init syntax highlighter - val highlight = Highlight() - val highlightWatcher = HighlightTextWatcher() - - val schemes = listOf( - ColorScheme(Pattern.compile("([\"'])(?:\\\\1|.)*?\\1"), Color.parseColor("#FC8500")), - ColorScheme(Pattern.compile("yt-dlp"), Color.parseColor("#00FF00")), - ColorScheme(Pattern.compile("(https?://(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?://(?:www\\.|(?!www))[a-zA-Z0-9]+\\.[^\\s]{2,}|www\\.[a-zA-Z0-9]+\\.[^\\s]{2,})"), Color.parseColor("#b5942f")), - ColorScheme(Pattern.compile("\\d+(\\.\\d)?%"), Color.parseColor("#43a564")) - ) - - highlight.addScheme( - *schemes.map { it }.toTypedArray() - ) - highlightWatcher.addScheme( - *schemes.map { it }.toTypedArray() - ) - highlight.setSpan(currentText) - currentText.addTextChangedListener(highlightWatcher) - - highlight.setSpan(text) - text.addTextChangedListener(highlightWatcher) + text.enableTextHighlight() + currentText.enableTextHighlight() text?.setText(item?.extraCommands) val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt index b003ad27..d6303e4a 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt @@ -28,7 +28,9 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.UiUtil -import com.google.android.material.chip.Chip +import com.deniscerri.ytdlnis.util.UiUtil.enableTextHighlight +import com.deniscerri.ytdlnis.util.UiUtil.populateCommandCard +import com.google.android.material.card.MaterialCardView import com.google.android.material.textfield.TextInputLayout import com.google.gson.Gson import kotlinx.coroutines.Dispatchers @@ -61,6 +63,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu } + @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) lifecycleScope.launch { @@ -79,7 +82,8 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu } val chosenCommandView = view.findViewById(R.id.content) - chosenCommandView.editText!!.setText(downloadItem.format.format_note) + chosenCommandView.editText?.setText(downloadItem.format.format_note) + chosenCommandView.editText?.enableTextHighlight() chosenCommandView.endIconDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_delete_all) chosenCommandView.editText!!.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} @@ -113,25 +117,29 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu } chosenCommandView.editText!!.enableScrollText() - val commandTemplates = view.findViewById(R.id.template) - val autoCompleteTextView = - requireView().findViewById(R.id.template_textview) - populateCommandTemplates(templates, autoCompleteTextView) + val commandTemplateCard = view.findViewById(R.id.command_card) + populateCommandCard(commandTemplateCard, templates.first()) - (commandTemplates!!.editText as AutoCompleteTextView?)!!.onItemClickListener = - AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, index: Int, _: Long -> - chosenCommandView.editText!!.setText(templates[index].content) - downloadItem.format = Format( - templates[index].title, - "", - "", - "", - "", - 0, - templates[index].content - ) + commandTemplateCard.setOnClickListener { + lifecycleScope.launch { + UiUtil.showCommandTemplates(requireActivity(), commandTemplateViewModel){ + chosenCommandView.editText!!.setText(it.content) + populateCommandCard(commandTemplateCard, it) + downloadItem.format = Format( + it.title, + "", + "", + "", + "", + 0, + it.content + ) + } } + + } + saveDir = view.findViewById(R.id.outputPath) saveDir.editText!!.setText( FileUtil.formatPath(downloadItem.downloadPath) @@ -151,9 +159,14 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace )) + val shortcutCount = withContext(Dispatchers.IO){ + commandTemplateViewModel.getTotalShortcutNumber() + } + UiUtil.configureCommand( view, 1, + shortcutCount, newTemplateClicked = { UiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) { templates.add(it) @@ -167,16 +180,24 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu 0, it.content ) - populateCommandTemplates(templates, autoCompleteTextView) + populateCommandCard(commandTemplateCard, it) } }, - editSelectedClicked = { - var current = templates.find { it.title == autoCompleteTextView.text.toString() } - if (current == null) current = CommandTemplate(0, "", chosenCommandView.editText!!.text.toString(), false) + editSelectedClicked = + { + var current = templates.find { it.id == commandTemplateCard.findViewById( + R.id.id).toString().toLong() } + if (current == null) current = + CommandTemplate( + 0, + "", + chosenCommandView.editText!!.text.toString(), + false + ) UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) { templates.add(it) chosenCommandView.editText!!.setText(it.content) - downloadItem.format = Format( + downloadItem.format = com.deniscerri.ytdlnis.database.models.Format( it.title, "", "", @@ -186,29 +207,22 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu it.content ) } + }, + shortcutClicked = { + UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel) { sh -> + chosenCommandView.editText!!.setText("${chosenCommandView.editText!!.text} $sh") + downloadItem.format.format_note += " $sh" + } } ) + + } catch (e: Exception) { e.printStackTrace() } } } - private fun populateCommandTemplates(templates: List, autoCompleteTextView: AutoCompleteTextView?){ - val templateTitles = templates.map {it.title} - - - autoCompleteTextView?.setAdapter( - ArrayAdapter( - requireContext(), - android.R.layout.simple_dropdown_item_1line, - templateTitles - ) - ) - if (templateTitles.isNotEmpty()) { - autoCompleteTextView!!.setText(downloadItem.format.format_id, false) - } - } private var commandPathResultLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/ActiveDownloadsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/ActiveDownloadsFragment.kt index 337e344d..50bfec2b 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/ActiveDownloadsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/ActiveDownloadsFragment.kt @@ -75,34 +75,27 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis pauseResume.setOnClickListener { if (pauseResume.text == requireContext().getString(R.string.pause)){ - lifecycleScope.launch { + workManager.cancelAllWorkByTag("download") pauseResume.isEnabled = false - val queued = withContext(Dispatchers.IO){ - downloadViewModel.getQueued() - } - queued.forEach { - it.status = DownloadRepository.Status.QueuedPaused.toString() - runBlocking { - downloadViewModel.updateDownload(it) - } - } - - queued.forEach { - workManager.cancelAllWorkByTag(it.id.toString()) - } - - val active = withContext(Dispatchers.IO){ + // pause active + withContext(Dispatchers.IO){ downloadViewModel.getActiveDownloads() - } - - active.forEach { + }.forEach { cancelItem(it.id.toInt()) it.status = DownloadRepository.Status.Paused.toString() downloadViewModel.updateDownload(it) } + // pause queued + withContext(Dispatchers.IO){ + downloadViewModel.getQueued() + }.forEach { + it.status = DownloadRepository.Status.QueuedPaused.toString() + downloadViewModel.updateDownload(it) + } + activeDownloads.notifyDataSetChanged() pauseResume.isEnabled = true } @@ -116,17 +109,22 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis val toQueue = active.filter { it.status == DownloadRepository.Status.Paused.toString() }.toMutableList() toQueue.forEach { - it.status = DownloadRepository.Status.Active.toString() + it.status = DownloadRepository.Status.Queued.toString() downloadViewModel.updateDownload(it) } + + runBlocking { + downloadViewModel.queueDownloads(listOf()) + } + val queuedItems = withContext(Dispatchers.IO){ downloadViewModel.getQueued() } - queuedItems.map { it.status = DownloadRepository.Status.Queued.toString() } - toQueue.addAll(queuedItems) - runBlocking { - downloadViewModel.queueDownloads(toQueue) + queuedItems.map { + it.status = DownloadRepository.Status.Queued.toString() + downloadViewModel.updateDownload(it) } + pauseResume.isEnabled = true } } @@ -220,20 +218,15 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis } activeDownloads.notifyItemChanged(position) - val count = withContext(Dispatchers.IO){ - downloadViewModel.getActiveDownloads() - }.count() - - val queue = if (count > 1) listOf(item) - else withContext(Dispatchers.IO){ - val list = downloadViewModel.getQueued().toMutableList() - list.map { it.status = DownloadRepository.Status.Queued.toString() } - list.add(0, item) - list + runBlocking { + downloadViewModel.queueDownloads(listOf(item)) } - runBlocking { - downloadViewModel.queueDownloads(queue) + withContext(Dispatchers.IO){ + downloadViewModel.getQueued().filter { it.status != DownloadRepository.Status.Queued.toString() }.forEach { + it.status = DownloadRepository.Status.Queued.toString() + downloadViewModel.updateDownload(it) + } } } } @@ -255,7 +248,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis private fun cancelItem(id: Int){ YoutubeDL.getInstance().destroyProcessById(id.toString()) - WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString()) notificationUtil.cancelDownloadNotification(id) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/TerminalActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/TerminalActivity.kt index 2502a49e..7ca68dfb 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/TerminalActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/TerminalActivity.kt @@ -35,6 +35,7 @@ import com.deniscerri.ytdlnis.ui.BaseActivity import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.UiUtil +import com.deniscerri.ytdlnis.util.UiUtil.enableTextHighlight import com.deniscerri.ytdlnis.work.TerminalDownloadWorker import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.bottomappbar.BottomAppBar @@ -230,27 +231,8 @@ class TerminalActivity : BaseActivity() { } initMenu() - //init syntax highlighter - val highlight = Highlight() - val highlightWatcher = HighlightTextWatcher() - - val schemes = listOf( - ColorScheme(Pattern.compile("([\"'])(?:\\\\1|.)*?\\1"), Color.parseColor("#FC8500")), - ColorScheme(Pattern.compile("yt-dlp"), Color.parseColor("#00FF00")), - ColorScheme(Pattern.compile("(https?://(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?://(?:www\\.|(?!www))[a-zA-Z0-9]+\\.[^\\s]{2,}|www\\.[a-zA-Z0-9]+\\.[^\\s]{2,})"), Color.parseColor("#b5942f")), - ColorScheme(Pattern.compile("\\d+(\\.\\d)?%"), Color.parseColor("#43a564")) - ) - - highlight.addScheme( - *schemes.map { it }.toTypedArray() - ) - highlightWatcher.addScheme( - *schemes.map { it }.toTypedArray() - ) - highlight.setSpan(output) - highlight.setSpan(input) - input?.addTextChangedListener(highlightWatcher) - output?.addTextChangedListener(highlightWatcher) + input?.enableTextHighlight() + output?.enableTextHighlight() } private fun initMenu() { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/FolderSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/FolderSettingsFragment.kt index 73b199b7..423d79ef 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/FolderSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/FolderSettingsFragment.kt @@ -113,14 +113,14 @@ class FolderSettingsFragment : BaseSettingsFragment() { audioFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]" audioFilenameTemplate?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]" - var cacheSize = File(FileUtil.getCachePath()).walkBottomUp().fold(0L) { acc, file -> acc + file.length() } + var cacheSize = File(FileUtil.getCachePath(requireContext())).walkBottomUp().fold(0L) { acc, file -> acc + file.length() } clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}" clearCache!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { if (activeDownloadCount == 0){ - File(FileUtil.getCachePath()).deleteRecursively() + File(FileUtil.getCachePath(requireContext())).deleteRecursively() Snackbar.make(requireView(), getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show() - cacheSize = File(FileUtil.getCachePath()).walkBottomUp().fold(0L) { acc, file -> acc + file.length() } + cacheSize = File(FileUtil.getCachePath(requireContext())).walkBottomUp().fold(0L) { acc, file -> acc + file.length() } clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}" }else{ Snackbar.make(requireView(), getString(R.string.downloads_running_try_later), Snackbar.LENGTH_SHORT).show() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt index d79515b1..41a9274d 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt @@ -157,6 +157,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() { "queued" -> json.add("queued", backupQueuedDownloads() ) "cancelled" -> json.add("cancelled", backupCancelledDownloads() ) "errored" -> json.add("errored", backupErroredDownloads() ) + "saved" -> json.add("saved", backupSavedDownloads() ) "cookies" -> json.add("cookies", backupCookies() ) "templates" -> json.add("templates", backupCommandTemplates() ) "shortcuts" -> json.add("shortcuts", backupShortcuts() ) @@ -293,6 +294,20 @@ class MainSettingsFragment : PreferenceFragmentCompat() { return JsonArray() } + private suspend fun backupSavedDownloads() : JsonArray { + runCatching { + val items = withContext(Dispatchers.IO) { + downloadViewModel.getSaved() + } + val arr = JsonArray() + items.forEach { + arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject) + } + return arr + } + return JsonArray() + } + private suspend fun backupCookies() : JsonArray { runCatching { val items = withContext(Dispatchers.IO) { @@ -484,6 +499,28 @@ class MainSettingsFragment : PreferenceFragmentCompat() { } } + //erorred downloads restore + if(json.has("saved")){ + val items = json.getAsJsonArray("saved") + val saved = mutableListOf() + items.forEach { + val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java) + item.id = 0L + saved.add(item) + + } + + saved.asReversed().forEach { f -> + withContext(Dispatchers.IO){ + downloadViewModel.insert(f) + } + } + + if(saved.isNotEmpty()){ + finalMessage.append("${getString(R.string.saved)}: ${saved.count()}\n") + } + } + //cookies restore if(json.has("cookies")){ val items = json.getAsJsonArray("cookies") diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt index 26353632..78a20f77 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt @@ -5,13 +5,9 @@ import android.media.MediaScannerConnection import android.net.Uri import android.os.Build import android.os.Environment -import android.os.storage.StorageManager -import android.os.storage.StorageVolume import android.provider.DocumentsContract import android.util.Log import android.webkit.MimeTypeMap -import androidx.core.content.FileProvider -import androidx.core.net.toFile import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import com.anggrayudi.storage.callback.FileCallback @@ -19,17 +15,15 @@ import com.anggrayudi.storage.callback.FolderCallback import com.anggrayudi.storage.file.copyFileTo import com.anggrayudi.storage.file.copyFolderTo import com.anggrayudi.storage.file.getAbsolutePath -import com.anggrayudi.storage.file.getBasePath import com.deniscerri.ytdlnis.App -import com.deniscerri.ytdlnis.BuildConfig import com.deniscerri.ytdlnis.R +import com.yausername.youtubedl_android.YoutubeDLRequest import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly import java.io.File import java.net.URLDecoder import java.nio.charset.StandardCharsets -import java.nio.file.CopyOption import java.nio.file.Files import java.nio.file.StandardCopyOption import java.text.DecimalFormat @@ -91,8 +85,7 @@ object FileUtil { if (it.isDirectory && it.absolutePath == originDir.absolutePath) return@forEach var destFile: DocumentFile try { - if (it.name.matches("(^config.*.\\.txt\$)|(rList)|(part-Frag)|(live_chat)".toRegex())){ - it.delete() + if (it.name.matches("(^config.*.\\.txt\$)|(rList)|(.*.part-Frag)|(.*.live_chat)".toRegex())){ return@forEach } @@ -269,8 +262,17 @@ object FileUtil { return listOf(context.getString(R.string.unfound_file)) } - fun getCachePath() : String { - return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/.CACHE" + fun getCachePath(context: Context) : String { + return context.cacheDir.absolutePath + "/downloads/" + } + + fun deleteConfigFiles(request: YoutubeDLRequest) { + request.getArguments("--config")?.forEach { + if (it != null) File(it).delete() + } + request.getArguments("--config-locations")?.forEach { + if (it != null) File(it).delete() + } } fun getDefaultAudioPath() : String{ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt index 02903f87..fb11ce81 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt @@ -13,7 +13,6 @@ import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel -import com.deniscerri.ytdlnis.work.DownloadWorker import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.yausername.youtubedl_android.YoutubeDL @@ -546,6 +545,7 @@ class InfoUtil(private val context: Context) { fun getFromYTDL(query: String): ArrayList { items = ArrayList() + val webpage_urls = mutableListOf() val searchEngine = sharedPreferences.getString("search_engine", "ytsearch") try { val request : YoutubeDLRequest @@ -610,12 +610,6 @@ class InfoUtil(private val context: Context) { } } - val url = if (jsonObject.has("url")){ - jsonObject.getString("url") - }else{ - jsonObject.getString("webpage_url") - } - var thumb: String? = "" if (jsonObject.has("thumbnail")) { thumb = jsonObject.getString("thumbnail") @@ -652,6 +646,12 @@ class InfoUtil(private val context: Context) { urls = urlList.joinToString("\n") } + val url = if (jsonObject.has("url")){ + jsonObject.getString("url") + }else{ + jsonObject.getString("webpage_url") + } + items.add(ResultItem(0, url, title, @@ -665,6 +665,10 @@ class InfoUtil(private val context: Context) { chapters ) ) + webpage_urls.add(jsonObject.getString("webpage_url")) + } + if (items.size == 1){ + items[0]?.url = webpage_urls.first() } } catch (e: Exception) { Looper.prepare().run { @@ -929,7 +933,7 @@ class InfoUtil(private val context: Context) { } fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ - val cacheDir = FileUtil.getCachePath() + val cacheDir = FileUtil.getCachePath(context) val tempFileDir = File(cacheDir, downloadItem.id.toString()) tempFileDir.delete() tempFileDir.mkdirs() @@ -1009,7 +1013,10 @@ class InfoUtil(private val context: Context) { request.addOption("--force-keyframes-at-cuts") } } - downloadItem.customFileNameTemplate += " %(section_title|)s %(autonumber)s" + downloadItem.customFileNameTemplate += " %(section_title|)s " + if (downloadItem.downloadSections.split(";").size > 1){ + downloadItem.customFileNameTemplate += "%(autonumber)s" + } request.addOption("--output-na-placeholder", " ") } @@ -1018,7 +1025,7 @@ class InfoUtil(private val context: Context) { } if (downloadItem.extraCommands.isNotBlank()){ - val conf = File(tempFileDir.absolutePath + "/${System.currentTimeMillis()}.txt") + val conf = File(FileUtil.getCachePath(context) + "/${System.currentTimeMillis()}.txt") conf.createNewFile() conf.writeText(downloadItem.extraCommands) request.addOption( @@ -1097,7 +1104,7 @@ class InfoUtil(private val context: Context) { if (! request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", "jpg") if (sharedPreferences.getBoolean("crop_thumbnail", true)){ try { - val config = File(tempFileDir.absolutePath + "/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt") + val config = File(context.cacheDir.absolutePath + "/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt") val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\"" config.writeText(configData) request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1") @@ -1129,39 +1136,18 @@ class InfoUtil(private val context: Context) { } if (downloadItem.videoPreferences.embedSubs) { request.addOption("--embed-subs") - request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages) + request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" }) } - val defaultFormats = context.resources.getStringArray(R.array.video_formats) - if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty()) request.addOption("--audio-multistreams") - - var videoFormatID = downloadItem.format.format_id - Log.e(DownloadWorker.TAG, videoFormatID) - var formatArgument = if (downloadItem.videoPreferences.removeAudio) "bestvideo" else "bestvideo+bestaudio/best" - if (videoFormatID.isNotEmpty()) { - if (videoFormatID == context.resources.getString(R.string.best_quality) || videoFormatID == "best") videoFormatID = "bestvideo" - else if (videoFormatID == context.resources.getString(R.string.worst_quality) || videoFormatID == "worst") videoFormatID = "worst" - else if (defaultFormats.contains(videoFormatID)) videoFormatID = "bestvideo[height<="+videoFormatID.substring(0, videoFormatID.length -1)+"]" - - formatArgument = if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty() && ! downloadItem.videoPreferences.removeAudio){ - val audioIds = downloadItem.videoPreferences.audioFormatIDs.joinToString("+") - "$videoFormatID+$audioIds/$videoFormatID/best" - }else{ - "$videoFormatID+bestaudio/$videoFormatID/best" - } - } - Log.e(DownloadWorker.TAG, formatArgument) - request.addOption("-f", formatArgument) - - if (downloadItem.format.container.equals("mpeg_4", true)) downloadItem.format.container = "mp4" + var cont = "" val outputContainer = downloadItem.container if( outputContainer.isNotEmpty() && outputContainer != "Default" && outputContainer != context.getString(R.string.defaultValue) && - supportedContainers.contains(outputContainer) && - !outputContainer.equals(downloadItem.format.container, true) + supportedContainers.contains(outputContainer) ){ + cont = outputContainer request.addOption("--merge-output-format", outputContainer.lowercase()) if (outputContainer != "webm") { val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) @@ -1171,6 +1157,103 @@ class InfoUtil(private val context: Context) { } } + //format logic + + val defaultFormats = context.resources.getStringArray(R.array.video_formats) + var usingGenericFormat = false + + var videof = downloadItem.format.format_id + if (videof == context.resources.getString(R.string.best_quality) || videof == "best") { + videof = "bestvideo" + usingGenericFormat = true + }else if (videof == context.resources.getString(R.string.worst_quality) || videof == "worst") { + videof = "worst" + }else if (defaultFormats.contains(videof)) { + videof = "bestvideo[height<="+videof.substring(0, videof.length -1)+"]" + } + + val preferredFormatIDs = sharedPreferences.getString("format_id", "").toString() + .split(",") + .filter { it.isNotEmpty() } + .ifEmpty { listOf(videof) } + + var audiof = "bestaudio" + if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty()){ + audiof = downloadItem.videoPreferences.audioFormatIDs.joinToString("+") + } + if (downloadItem.videoPreferences.removeAudio) audiof = "" + + val preferredAudioFormatIDs = sharedPreferences.getString("format_id_audio", "") + .toString() + .split(",") + .filter { it.isNotEmpty() } + .ifEmpty { listOf(audiof) } + + val preferredVideoFormats = if (usingGenericFormat) preferredFormatIDs else listOf(videof) + val preferredAudioFormats = if (usingGenericFormat && downloadItem.videoPreferences.audioFormatIDs.isEmpty()) { + preferredAudioFormatIDs + } else listOf(audiof) + + if (preferredAudioFormats.any{it.contains("+")}){ + request.addOption("--audio-multistreams") + } + + val preferredCodec = sharedPreferences.getString("video_codec", "") + val extPref = if (cont.isNotBlank()) "[ext=$cont]" else "" + val vCodecPref = if (preferredCodec!!.isNotBlank()) "[vcodec^=$preferredCodec]" else "" + + val f = StringBuilder() + + //build format with extension and vcodec + preferredVideoFormats.forEach { v -> + preferredAudioFormats.forEach { a -> + val aa = if (a.isNotBlank()) "+$a" else "" + f.append("$v$extPref$vCodecPref$aa/") + } + } + + //build format with vcodec + if (extPref.isNotBlank()){ + preferredVideoFormats.forEach { v -> + preferredAudioFormats.forEach { a -> + val aa = if (a.isNotBlank()) "+$a" else "" + f.append("$videof$vCodecPref$aa/") + } + } + } + + if (vCodecPref.isNotBlank()){ + preferredVideoFormats.forEach { v -> + //build format with vcodec + preferredAudioFormats.forEach {a -> + val aa = if (a.isNotBlank()) "+$a" else "" + f.append("$v$aa/") + } + } + } + + //build format with best audio + preferredVideoFormats.forEach { v -> + if(!f.contains("$v+bestaudio/")){ + f.append("$v+bestaudio/") + } + } + + + //build formats with standalone video + preferredVideoFormats.forEach { v -> + if(!f.contains("/$v/")){ + f.append("$v/") + } + } + + if(!f.endsWith("best/")){ + //last fallback + f.append("best") + } + + request.addOption("-f", f.toString().replace("/$".toRegex(), "")) + if (downloadItem.videoPreferences.writeSubs){ val subFormat = sharedPreferences.getString("sub_format", "srt") @@ -1179,7 +1262,7 @@ class InfoUtil(private val context: Context) { request.addOption("--sub-format", "${subFormat}/best") request.addOption("--convert-subtitles", "srt") if (!downloadItem.videoPreferences.embedSubs) { - request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages) + request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" }) } } @@ -1202,7 +1285,7 @@ class InfoUtil(private val context: Context) { DownloadViewModel.Type.command -> { request.addOption( "--config-locations", - File(FileUtil.getCachePath() + "/config[${downloadItem.id}].txt").apply { + File(context.cacheDir.absolutePath + "/config[${downloadItem.id}].txt").apply { writeText(downloadItem.format.format_note) }.absolutePath ) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt index adf5bda1..21176eb6 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt @@ -15,6 +15,7 @@ import androidx.core.content.FileProvider import androidx.navigation.NavDeepLinkBuilder import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.receiver.CancelDownloadNotificationReceiver +import com.deniscerri.ytdlnis.receiver.CancelWorkReceiver import com.deniscerri.ytdlnis.receiver.PauseDownloadNotificationReceiver import com.deniscerri.ytdlnis.receiver.ResumeActivity import com.deniscerri.ytdlnis.receiver.ShareFileActivity @@ -81,7 +82,7 @@ class NotificationUtil(var context: Context) { return notificationBuilder .setContentTitle(context.getString(R.string.downloading)) .setOngoing(true) - .setSmallIcon(R.drawable.ic_launcher_foreground) + .setSmallIcon(R.drawable.ic_launcher_foreground_large) .setLargeIcon( BitmapFactory.decodeResource( context.resources, @@ -98,30 +99,11 @@ class NotificationUtil(var context: Context) { fun createDownloadServiceNotification( pendingIntent: PendingIntent?, title: String?, - workID: Int, - channel: String + itemID: Int, ): Notification { - val notificationBuilder = getBuilder(channel) + val notificationBuilder = getBuilder(DOWNLOAD_SERVICE_CHANNEL_ID) - val pauseIntent = Intent(context, PauseDownloadNotificationReceiver::class.java) - pauseIntent.putExtra("workID", workID) - pauseIntent.putExtra("title", title) - val pauseNotificationPendingIntent = PendingIntent.getBroadcast( - context, - workID, - pauseIntent, - PendingIntent.FLAG_IMMUTABLE - ) - val cancelIntent = Intent(context, CancelDownloadNotificationReceiver::class.java) - cancelIntent.putExtra("cancel", "") - cancelIntent.putExtra("workID", workID) - val cancelNotificationPendingIntent = PendingIntent.getBroadcast( - context, - workID, - cancelIntent, - PendingIntent.FLAG_IMMUTABLE - ) return notificationBuilder .setContentTitle(title) .setOngoing(true) @@ -140,13 +122,11 @@ class NotificationUtil(var context: Context) { .setContentIntent(pendingIntent) .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .clearActions() - .addAction(0, context.getString(R.string.pause), pauseNotificationPendingIntent) - .addAction(0, context.getString(R.string.cancel), cancelNotificationPendingIntent) .build() } - fun createResumeDownload(workID: Int, title: String?, channel: String){ - val notificationBuilder = getBuilder(channel) + fun createResumeDownload(itemID: Int, title: String?){ + val notificationBuilder = getBuilder(DOWNLOAD_SERVICE_CHANNEL_ID) notificationBuilder .setContentTitle(title) @@ -162,16 +142,16 @@ class NotificationUtil(var context: Context) { .clearActions() val intent = Intent(context, ResumeActivity::class.java) - intent.putExtra("workID", workID) + intent.putExtra("itemID", itemID) val resumeNotificationPendingIntent = PendingIntent.getActivity( context, - workID, + itemID, intent, PendingIntent.FLAG_IMMUTABLE ) notificationBuilder.addAction(0, context.getString(R.string.resume), resumeNotificationPendingIntent) - notificationManager.notify(DOWNLOAD_RESUME_NOTIFICATION_ID, notificationBuilder.build()) + notificationManager.notify(DOWNLOAD_RESUME_NOTIFICATION_ID + itemID, notificationBuilder.build()) } fun createUpdatingItemNotification(channel: String){ @@ -315,10 +295,33 @@ class NotificationUtil(var context: Context) { var contentText = "" if (queue > 1) contentText += """${queue - 1} ${context.getString(R.string.items_left)}""" + "\n" contentText += desc.replace("\\[.*?\\] ".toRegex(), "") + + val pauseIntent = Intent(context, PauseDownloadNotificationReceiver::class.java) + pauseIntent.putExtra("itemID", id) + pauseIntent.putExtra("title", title) + val pauseNotificationPendingIntent = PendingIntent.getBroadcast( + context, + id, + pauseIntent, + PendingIntent.FLAG_IMMUTABLE + ) + + val cancelIntent = Intent(context, CancelDownloadNotificationReceiver::class.java) + cancelIntent.putExtra("itemID", id) + val cancelNotificationPendingIntent = PendingIntent.getBroadcast( + context, + id, + cancelIntent, + PendingIntent.FLAG_IMMUTABLE + ) + try { notificationBuilder.setProgress(100, progress, false) .setContentTitle(title) .setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) + .clearActions() + .addAction(0, context.getString(R.string.pause), pauseNotificationPendingIntent) + .addAction(0, context.getString(R.string.cancel), cancelNotificationPendingIntent) notificationManager.notify(id, notificationBuilder.build()) } catch (e: Exception) { e.printStackTrace() @@ -388,8 +391,7 @@ class NotificationUtil(var context: Context) { fun createFormatsUpdateNotification(workID: Int): Notification { val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) - val cancelIntent = Intent(context, CancelDownloadNotificationReceiver::class.java) - cancelIntent.putExtra("cancel", "") + val cancelIntent = Intent(context, CancelWorkReceiver::class.java) cancelIntent.putExtra("workID", workID) val cancelNotificationPendingIntent = PendingIntent.getBroadcast( context, @@ -473,7 +475,7 @@ class NotificationUtil(var context: Context) { const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2" const val DOWNLOAD_FINISHED_CHANNEL_ID = "3" const val DOWNLOAD_FINISHED_NOTIFICATION_ID = 3 - const val DOWNLOAD_RESUME_NOTIFICATION_ID = 4 + const val DOWNLOAD_RESUME_NOTIFICATION_ID = 40000 const val DOWNLOAD_UPDATING_NOTIFICATION_ID = 5 const val FORMAT_UPDATING_NOTIFICATION_ID = 6 const val FORMAT_UPDATING_FINISHED_NOTIFICATION_ID = 7 diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt index eefc09a4..46de315a 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt @@ -7,6 +7,7 @@ import android.content.ClipboardManager import android.content.Context import android.content.DialogInterface import android.content.Intent +import android.graphics.Color import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.OvalShape import android.net.Uri @@ -30,7 +31,6 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources -import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.view.isVisible @@ -62,13 +62,18 @@ import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.textfield.TextInputLayout import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat +import com.neo.highlight.core.Highlight +import com.neo.highlight.util.listener.HighlightTextWatcher +import com.neo.highlight.util.scheme.ColorScheme import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import me.zhanghai.android.fastscroll.FastScrollerBuilder import java.io.File import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale +import java.util.regex.Pattern object UiUtil { @SuppressLint("SetTextI18n") @@ -85,7 +90,7 @@ object UiUtil { if (audioFormats.isNullOrEmpty()){ formatCard.findViewById(R.id.format_note).text = formatNote.uppercase() }else{ - val title = "${formatNote.uppercase()} + [${audioFormats.joinToString("/") { it.format_note }}]" + val title = "${formatNote.uppercase()} + [${audioFormats.joinToString("/") { "(${it.format_id}) ${it.format_note}" }}]" formatCard.findViewById(R.id.format_note).text = title } formatCard.findViewById(R.id.format_id).text = "id: ${chosenFormat.format_id}" @@ -109,6 +114,12 @@ object UiUtil { } + fun populateCommandCard(card: MaterialCardView, item: CommandTemplate){ + card.findViewById(R.id.title).text = item.title + card.findViewById(R.id.content).text = item.content + card.findViewById(R.id.id).text = item.id.toString() + } + fun showCommandTemplateCreationOrUpdatingSheet(item: CommandTemplate?, context: Activity, lifeCycle: LifecycleOwner, commandTemplateViewModel: CommandTemplateViewModel, newTemplate: (newTemplate: CommandTemplate) -> Unit){ val bottomSheet = BottomSheetDialog(context) bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) @@ -658,7 +669,7 @@ object UiUtil { linearLayout!!.removeAllViews() list.forEach {template -> - val item = activity.layoutInflater.inflate(R.layout.command_template_item, linearLayout, false) as ConstraintLayout + val item = activity.layoutInflater.inflate(R.layout.command_template_item, linearLayout, false) as MaterialCardView item.findViewById(R.id.title).text = template.title item.findViewById(R.id.content).text = template.content item.setOnClickListener { @@ -719,8 +730,12 @@ object UiUtil { extraCommandsClicked: () -> Unit ){ val embedSubs = view.findViewById(R.id.embed_subtitles) + val saveSubtitles = view.findViewById(R.id.save_subtitles) + val subtitleLanguages = view.findViewById(R.id.subtitle_languages) + embedSubs!!.isChecked = items.all { it.videoPreferences.embedSubs } embedSubs.setOnClickListener { + subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked embedSubsClicked(embedSubs.isChecked) } @@ -878,8 +893,6 @@ object UiUtil { dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START } - val saveSubtitles = view.findViewById(R.id.save_subtitles) - val subtitleLanguages = view.findViewById(R.id.subtitle_languages) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) @@ -890,11 +903,11 @@ object UiUtil { } saveSubtitles.setOnCheckedChangeListener { _, _ -> - if (saveSubtitles.isChecked) subtitleLanguages.visibility = View.VISIBLE - else subtitleLanguages.visibility = View.GONE + subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked saveSubtitlesClicked(saveSubtitles.isChecked) } + subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked subtitleLanguages.setOnClickListener { subtitleLanguagesClicked() } @@ -1074,8 +1087,10 @@ object UiUtil { fun configureCommand( view: View, size: Int, + shortcutCount: Int, newTemplateClicked: () -> Unit, editSelectedClicked: () -> Unit, + shortcutClicked: suspend () -> Unit, ){ val newTemplate : Chip = view.findViewById(R.id.newTemplate) newTemplate.setOnClickListener { @@ -1087,6 +1102,14 @@ object UiUtil { editSelected.setOnClickListener { editSelectedClicked() } + + val shortcuts = view.findViewById(R.id.shortcut) + shortcuts.isEnabled = shortcutCount > 0 + shortcuts.setOnClickListener { + runBlocking { + shortcutClicked() + } + } } @@ -1117,6 +1140,31 @@ object UiUtil { .build() } + private var textHighLightSchemes = listOf( + ColorScheme(Pattern.compile("([\"'])(?:\\\\1|.)*?\\1"), Color.parseColor("#FC8500")), + ColorScheme(Pattern.compile("yt-dlp"), Color.parseColor("#00FF00")), + ColorScheme(Pattern.compile("(https?://(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?://(?:www\\.|(?!www))[a-zA-Z0-9]+\\.[^\\s]{2,}|www\\.[a-zA-Z0-9]+\\.[^\\s]{2,})"), Color.parseColor("#b5942f")), + ColorScheme(Pattern.compile("\\d+(\\.\\d)?%"), Color.parseColor("#43a564")) + ) + + fun View.enableTextHighlight(){ + if (this is EditText || this is TextView){ + //init syntax highlighter + val highlight = Highlight() + val highlightWatcher = HighlightTextWatcher() + + highlight.addScheme( + *textHighLightSchemes.map { it }.toTypedArray() + ) + highlightWatcher.addScheme( + *textHighLightSchemes.map { it }.toTypedArray() + ) + + highlight.setSpan(this as TextView) + this.addTextChangedListener(highlightWatcher) + } + } + diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt index 6c5f5a48..28127192 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt @@ -31,8 +31,6 @@ import com.yausername.youtubedl_android.YoutubeDL import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -61,29 +59,34 @@ class DownloadWorker( val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await() if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success() - runningYTDLInstances.addAll(dao.getActiveDownloadsList().map { it.id }) - val pendingIntent = NavDeepLinkBuilder(context) .setGraph(R.navigation.nav_graph) .setDestination(R.id.downloadQueueMainFragment) .createPendingIntent() - var notification = notificationUtil.createDefaultWorkerNotification() - val foregroundInfo = ForegroundInfo(Random.nextInt(1000000000), notification) + val workNotif = notificationUtil.createDefaultWorkerNotification() + val foregroundInfo = ForegroundInfo(Random.nextInt(1000000000), workNotif) setForegroundAsync(foregroundInfo) queuedItems.collectLatest { items -> + runningYTDLInstances.clear() + dao.getActiveDownloadsList().forEach { + runningYTDLInstances.add(it.id) + } + val running = ArrayList(runningYTDLInstances) if (items.isEmpty() && running.isEmpty()) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) + val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size val eligibleDownloads = items.take(if (concurrentDownloads < 0) 0 else concurrentDownloads).filter { it.id !in running } - eligibleDownloads.forEach {downloadItem -> + + eligibleDownloads.forEach{downloadItem -> runningYTDLInstances.add(downloadItem.id) + val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt()) + notificationUtil.notify(downloadItem.id.toInt(), notification) + + CoroutineScope(Dispatchers.IO).launch { - withContext(Dispatchers.Main){ - notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title, downloadItem.id.toInt(), NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID) - notificationUtil.notify(downloadItem.id.toInt(), notification) - } val request = infoUtil.buildYoutubeDLRequest(downloadItem) downloadItem.status = DownloadRepository.Status.Active.toString() CoroutineScope(Dispatchers.IO).launch { @@ -91,7 +94,7 @@ class DownloadWorker( updateDownloadItem(downloadItem, infoUtil, dao, resultDao) } - val cacheDir = FileUtil.getCachePath() + val cacheDir = FileUtil.getCachePath(context) val tempFileDir = File(cacheDir, downloadItem.id.toString()) tempFileDir.delete() tempFileDir.mkdirs() @@ -126,6 +129,7 @@ class DownloadWorker( } } + runCatching { YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line.chunked(5000).first().toString(), "id" to downloadItem.id)) @@ -143,6 +147,8 @@ class DownloadWorker( } }.onSuccess { runningYTDLInstances.remove(downloadItem.id) + FileUtil.deleteConfigFiles(request) + val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao) runBlocking { var finalPaths : List? @@ -209,6 +215,8 @@ class DownloadWorker( }.onFailure { runningYTDLInstances.remove(downloadItem.id) + FileUtil.deleteConfigFiles(request) + withContext(Dispatchers.Main){ notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) } @@ -246,6 +254,7 @@ class DownloadWorker( } } } + if (eligibleDownloads.isNotEmpty()){ eligibleDownloads.forEach { it.status = DownloadRepository.Status.Active.toString() } dao.updateMultiple(eligibleDownloads) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt index d103f6f8..8b8170bd 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt @@ -51,7 +51,7 @@ class TerminalDownloadWorker( val intent = Intent(context, TerminalActivity::class.java) val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) - val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, command.take(65), itemId, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID) + val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, command.take(65), itemId) val foregroundInfo = ForegroundInfo(itemId, notification) setForegroundAsync(foregroundInfo) @@ -65,7 +65,7 @@ class TerminalDownloadWorker( request.addOption( "--config-locations", - File(context.cacheDir.absolutePath + "/config${System.currentTimeMillis()}.txt").apply { + File(context.cacheDir.absolutePath + "/config-TERMINAL[${System.currentTimeMillis()}].txt").apply { writeText(command) }.absolutePath ) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/UpdateYTDLWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/UpdateYTDLWorker.kt index 5f99b578..b85fc73e 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/UpdateYTDLWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/UpdateYTDLWorker.kt @@ -13,9 +13,6 @@ class UpdateYTDLWorker( workerParams: WorkerParameters ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { - val notification = NotificationUtil(context).createYTDLUpdateNotification() - val foregroundInfo = ForegroundInfo(System.currentTimeMillis().toInt(), notification) - setForeground(foregroundInfo) UpdateUtil(context).updateYoutubeDL() return Result.success() } diff --git a/app/src/main/res/layout/adjust_command.xml b/app/src/main/res/layout/adjust_command.xml index 4a2507e6..0a7020d5 100644 --- a/app/src/main/res/layout/adjust_command.xml +++ b/app/src/main/res/layout/adjust_command.xml @@ -1,11 +1,10 @@ - + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + - \ No newline at end of file + diff --git a/app/src/main/res/layout/adjust_video.xml b/app/src/main/res/layout/adjust_video.xml index a632881a..03b0cd43 100644 --- a/app/src/main/res/layout/adjust_video.xml +++ b/app/src/main/res/layout/adjust_video.xml @@ -179,7 +179,7 @@ style="@style/Widget.Material3.Chip.Assist.Elevated" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="gone" + android:enabled="false" android:checked="false" android:text="@string/subtitle_languages"/> diff --git a/app/src/main/res/layout/command_template_item.xml b/app/src/main/res/layout/command_template_item.xml index 52caf6f6..37b155b6 100644 --- a/app/src/main/res/layout/command_template_item.xml +++ b/app/src/main/res/layout/command_template_item.xml @@ -1,57 +1,76 @@ - + android:checkable="true" + android:clickable="true" + android:focusable="true" + android:backgroundTint="@android:color/transparent" + app:checkedIcon="@null" + app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" + app:strokeWidth="0dp" + app:cardPreventCornerOverlap="true" + android:layout_height="wrap_content"> - + - + - + - + + + + + + + diff --git a/app/src/main/res/layout/configure_download_bottom_sheet.xml b/app/src/main/res/layout/configure_download_bottom_sheet.xml index da21bd3a..ebfc8e37 100644 --- a/app/src/main/res/layout/configure_download_bottom_sheet.xml +++ b/app/src/main/res/layout/configure_download_bottom_sheet.xml @@ -92,7 +92,7 @@ style="@style/Widget.Material3.Button.TextButton.Icon" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="10dp" + android:layout_marginHorizontal="10dp" android:textAlignment="textStart" android:singleLine="true" android:text="@string/app_name" diff --git a/app/src/main/res/layout/format_details_sheet.xml b/app/src/main/res/layout/format_details_sheet.xml index 0c0eaf08..cf6e87a4 100644 --- a/app/src/main/res/layout/format_details_sheet.xml +++ b/app/src/main/res/layout/format_details_sheet.xml @@ -5,13 +5,13 @@ @@ -23,24 +23,23 @@ android:paddingHorizontal="10dp" android:weightSum="100" android:background="?android:attr/selectableItemBackground" - android:orientation="vertical" > + android:orientation="horizontal" > @@ -54,26 +53,26 @@ android:paddingHorizontal="10dp" android:weightSum="100" android:background="?android:attr/selectableItemBackground" - android:orientation="vertical" > + android:orientation="horizontal" > @@ -87,23 +86,23 @@ android:weightSum="100" android:paddingHorizontal="10dp" android:background="?android:attr/selectableItemBackground" - android:orientation="vertical" > + android:orientation="horizontal" > @@ -117,22 +116,22 @@ android:weightSum="100" android:paddingHorizontal="10dp" android:background="?android:attr/selectableItemBackground" - android:orientation="vertical" > + android:orientation="horizontal" > @@ -146,22 +145,22 @@ android:weightSum="100" android:paddingHorizontal="10dp" android:background="?android:attr/selectableItemBackground" - android:orientation="vertical" > + android:orientation="horizontal" > @@ -175,22 +174,22 @@ android:weightSum="100" android:paddingHorizontal="10dp" android:background="?android:attr/selectableItemBackground" - android:orientation="vertical" > + android:orientation="horizontal" > @@ -204,22 +203,22 @@ android:weightSum="100" android:paddingHorizontal="10dp" android:background="?android:attr/selectableItemBackground" - android:orientation="vertical" > + android:orientation="horizontal" > @@ -233,22 +232,22 @@ android:weightSum="100" android:paddingHorizontal="10dp" android:background="?android:attr/selectableItemBackground" - android:orientation="vertical" > + android:orientation="horizontal" > diff --git a/app/src/main/res/layout/fragment_download_command.xml b/app/src/main/res/layout/fragment_download_command.xml index fa4e287b..b200e5dc 100644 --- a/app/src/main/res/layout/fragment_download_command.xml +++ b/app/src/main/res/layout/fragment_download_command.xml @@ -7,11 +7,11 @@ - + android:paddingHorizontal="10dp" + android:paddingTop="10dp" + android:text="@string/command" /> - - - + @@ -62,7 +56,7 @@ diff --git a/app/src/main/res/xml/folders_preference.xml b/app/src/main/res/xml/folders_preference.xml index 13f9ba76..c2bb225d 100644 --- a/app/src/main/res/xml/folders_preference.xml +++ b/app/src/main/res/xml/folders_preference.xml @@ -49,7 +49,6 @@ diff --git a/app/src/main/res/xml/processing_preferences.xml b/app/src/main/res/xml/processing_preferences.xml index e788f766..7b1a5afd 100644 --- a/app/src/main/res/xml/processing_preferences.xml +++ b/app/src/main/res/xml/processing_preferences.xml @@ -56,7 +56,7 @@ android:icon="@drawable/baseline_subject_24" app:key="subs_lang" app:useSimpleSummaryProvider="true" - app:defaultValue="all" + app:defaultValue="en.*,.*-orig" app:title="@string/subtitle_languages" />