This commit is contained in:
zaednasr 2024-07-28 23:33:06 +02:00
parent 6ba8d65afd
commit d57ab39e82
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
26 changed files with 378 additions and 199 deletions

View file

@ -10,7 +10,7 @@ plugins {
def properties = new Properties() def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 7 def versionMinor = 7
def versionPatch = 8 def versionPatch = 9
def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def isBeta = false def isBeta = false
def versionExt = isBeta ? ".${versionBuild}-beta" : "" def versionExt = isBeta ? ".${versionBuild}-beta" : ""

View file

@ -45,6 +45,11 @@ interface DownloadDao {
""") """)
fun getProcessingDownloadTypes() : List<String> fun getProcessingDownloadTypes() : List<String>
@Query("""
SELECT DISTINCT container from downloads where status = 'Processing'
""")
fun getProcessingDownloadContainers() : List<String>
@Query("UPDATE downloads set status = 'Processing' WHERE id in (:ids)") @Query("UPDATE downloads set status = 'Processing' WHERE id in (:ids)")
suspend fun updateItemsToProcessing(ids: List<Long>) suspend fun updateItemsToProcessing(ids: List<Long>)
@ -60,6 +65,9 @@ interface DownloadDao {
@Query("UPDATE downloads set downloadPath=:path WHERE status ='Processing'") @Query("UPDATE downloads set downloadPath=:path WHERE status ='Processing'")
suspend fun updateProcessingDownloadPath(path: String) suspend fun updateProcessingDownloadPath(path: String)
@Query("UPDATE downloads set container=:cont WHERE status ='Processing'")
suspend fun updateProcessingContainer(cont: String)
@Query("SELECT * FROM downloads WHERE status='Active'") @Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloadsList() : List<DownloadItem> fun getActiveDownloadsList() : List<DownloadItem>

View file

@ -7,7 +7,6 @@ import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import com.deniscerri.ytdl.database.models.LogItem import com.deniscerri.ytdl.database.models.LogItem
import com.deniscerri.ytdl.util.Extensions.appendLineToLog
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@ -41,10 +40,15 @@ interface LogDao {
suspend fun updateLogContent(l: String, id: Long) suspend fun updateLogContent(l: String, id: Long)
@Transaction @Transaction
suspend fun updateLog(line: String, id: Long) { suspend fun updateLog(line: String, id: Long, resetLog: Boolean = false) {
val l = getByID(id) ?: return val l = getByID(id) ?: return
val log = l.content ?: "" var log = l.content ?: ""
updateLogContent(log.appendLineToLog(line), id) if (resetLog) {
log = line.lines().joinToString("\n")
}else{
log += "\n${line}"
}
updateLogContent(log, id)
} }
@Update(onConflict = OnConflictStrategy.REPLACE) @Update(onConflict = OnConflictStrategy.REPLACE)

View file

@ -5,7 +5,6 @@ import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import com.deniscerri.ytdl.database.models.TerminalItem import com.deniscerri.ytdl.database.models.TerminalItem
import com.deniscerri.ytdl.util.Extensions.appendLineToLog
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@ -29,10 +28,15 @@ interface TerminalDao {
@Query("SELECT * FROM terminalDownloads WHERE id=:id LIMIT 1") @Query("SELECT * FROM terminalDownloads WHERE id=:id LIMIT 1")
fun getTerminalById(id: Long) : TerminalItem? fun getTerminalById(id: Long) : TerminalItem?
@Transaction @Transaction
suspend fun updateLog(line: String, id: Long){ suspend fun updateLog(line: String, id: Long, resetLog : Boolean = false){
val t = getTerminalById(id) ?: return val t = getTerminalById(id) ?: return
val log = t.log ?: "" var log = t.log ?: ""
updateTerminalLog(log.appendLineToLog(line), id) if (resetLog) {
log = line.lines().joinToString("\n")
}else{
log += "\n${line}"
}
updateTerminalLog(log, id)
} }
@Insert @Insert

View file

@ -13,6 +13,7 @@ data class DownloadItemConfigureMultiple(
var title: String, var title: String,
var thumb: String, var thumb: String,
var duration: String, var duration: String,
var container: String,
var format: Format, var format: Format,
var allFormats: MutableList<Format>, var allFormats: MutableList<Format>,
var audioPreferences: AudioPreferences, var audioPreferences: AudioPreferences,

View file

@ -2,7 +2,6 @@ package com.deniscerri.ytdl.database.repository
import com.deniscerri.ytdl.database.dao.LogDao import com.deniscerri.ytdl.database.dao.LogDao
import com.deniscerri.ytdl.database.models.LogItem import com.deniscerri.ytdl.database.models.LogItem
import com.deniscerri.ytdl.util.Extensions.appendLineToLog
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class LogRepository(private val logDao: LogDao) { class LogRepository(private val logDao: LogDao) {
@ -38,9 +37,9 @@ class LogRepository(private val logDao: LogDao) {
return logDao.getByID(id) return logDao.getByID(id)
} }
suspend fun update(line: String, id: Long){ suspend fun update(line: String, id: Long, resetLog: Boolean = false){
kotlin.runCatching { kotlin.runCatching {
logDao.updateLog(line, id) logDao.updateLog(line, id, resetLog)
} }
} }

View file

@ -1015,6 +1015,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
} }
suspend fun updateProcessingContainer(cont: String) {
var container = ""
if (cont != resources.getString(R.string.defaultValue)) {
container = cont
}
dao.updateProcessingContainer(container)
}
suspend fun updateProcessingDownloadPath(path: String){ suspend fun updateProcessingDownloadPath(path: String){
dao.updateProcessingDownloadPath(path) dao.updateProcessingDownloadPath(path)
} }
@ -1111,6 +1119,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return Pair(types.size == 1, Type.valueOf(types.first())) return Pair(types.size == 1, Type.valueOf(types.first()))
} }
fun checkIfAllProcessingItemsHaveSameContainer() : Pair<Boolean, String> {
val containers = dao.getProcessingDownloadContainers()
return Pair(containers.size == 1, containers.first())
}
suspend fun updateItemsWithIdsToProcessingStatus(ids: List<Long>) { suspend fun updateItemsWithIdsToProcessingStatus(ids: List<Long>) {
repository.deleteProcessing() repository.deleteProcessing()

View file

@ -175,6 +175,14 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
return repository.getItemByURL(url) return repository.getItemByURL(url)
} }
fun getAllByURL(url: String) : List<ResultItem> {
return repository.getAllByURL(url)
}
fun getByID(id: Long) : ResultItem? {
return repository.getItemByID(id)
}
fun addSearchQueryToHistory(query: String) = viewModelScope.launch(Dispatchers.IO) { fun addSearchQueryToHistory(query: String) = viewModelScope.launch(Dispatchers.IO) {
val allQueries = searchHistoryRepository.getAll() val allQueries = searchHistoryRepository.getAll()
if (allQueries.none { it.query == query }){ if (allQueries.none { it.query == query }){

View file

@ -33,6 +33,7 @@ import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.viewmodel.CookieViewModel import com.deniscerri.ytdl.database.viewmodel.CookieViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
@ -185,13 +186,18 @@ class ShareActivity : BaseActivity() {
val command = intent.getStringExtra("COMMAND") ?: "" val command = intent.getStringExtra("COMMAND") ?: ""
lifecycleScope.launch { lifecycleScope.launch {
var result = withContext(Dispatchers.IO){ val result: ResultItem
resultViewModel.getItemByURL(inputQuery) val existingResults = withContext(Dispatchers.IO){
resultViewModel.getAllByURL(inputQuery)
} }
if (result == null) {
if (existingResults.isEmpty() || existingResults.size > 1) {
resultViewModel.deleteAll() resultViewModel.deleteAll()
result = downloadViewModel.createEmptyResultItem(inputQuery) result = downloadViewModel.createEmptyResultItem(inputQuery)
}else{
result = existingResults.first()
} }
val downloadType = DownloadViewModel.Type.valueOf(type ?: downloadViewModel.getDownloadType(url = result.url).toString()) val downloadType = DownloadViewModel.Type.valueOf(type ?: downloadViewModel.getDownloadType(url = result.url).toString())
if (sharedPreferences.getBoolean("download_card", true) && !background){ if (sharedPreferences.getBoolean("download_card", true) && !background){
val bundle = Bundle() val bundle = Bundle()

View file

@ -45,6 +45,7 @@ import com.deniscerri.ytdl.ui.adapter.HomeAdapter
import com.deniscerri.ytdl.ui.adapter.SearchSuggestionsAdapter import com.deniscerri.ytdl.ui.adapter.SearchSuggestionsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.ThemeUtil import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout import com.facebook.shimmer.ShimmerFrameLayout
@ -84,6 +85,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
private var clipboardFab: ExtendedFloatingActionButton? = null private var clipboardFab: ExtendedFloatingActionButton? = null
private var homeFabs: LinearLayout? = null private var homeFabs: LinearLayout? = null
private var infoUtil: InfoUtil? = null private var infoUtil: InfoUtil? = null
private var notificationUtil: NotificationUtil? = null
private var downloadQueue: ArrayList<ResultItem>? = null private var downloadQueue: ArrayList<ResultItem>? = null
private lateinit var resultViewModel : ResultViewModel private lateinit var resultViewModel : ResultViewModel
@ -121,6 +123,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
mainActivity = activity as MainActivity? mainActivity = activity as MainActivity?
quickLaunchSheet = false quickLaunchSheet = false
infoUtil = InfoUtil(requireContext()) infoUtil = InfoUtil(requireContext())
notificationUtil = NotificationUtil(requireContext())
selectedObjects = arrayListOf() selectedObjects = arrayListOf()
return fragmentView return fragmentView
} }
@ -306,6 +309,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} }
if (arguments?.getBoolean("showDownloadsWithUpdatedFormats") == true){ if (arguments?.getBoolean("showDownloadsWithUpdatedFormats") == true){
notificationUtil?.cancelDownloadNotification(NotificationUtil.FORMAT_UPDATING_FINISHED_NOTIFICATION_ID)
arguments?.remove("showDownloadsWithUpdatedFormats") arguments?.remove("showDownloadsWithUpdatedFormats")
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val ids = arguments?.getLongArray("downloadIds") ?: return@launch val ids = arguments?.getLongArray("downloadIds") ?: return@launch

View file

@ -112,6 +112,10 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
codec.text = codecText codec.text = codecText
} }
val container = card.findViewById<TextView>(R.id.container)
container.isVisible = item.container.isNotBlank()
container.text = item.container.uppercase()
val fileSize = card.findViewById<TextView>(R.id.file_size) val fileSize = card.findViewById<TextView>(R.id.file_size)
val fileSizeReadable = if(item.type != DownloadViewModel.Type.video){ val fileSizeReadable = if(item.type != DownloadViewModel.Type.video){
FileUtil.convertFileSize(item.format.filesize) FileUtil.convertFileSize(item.format.filesize)
@ -185,6 +189,7 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
override fun areContentsTheSame(oldItem: DownloadItemConfigureMultiple, newItem: DownloadItemConfigureMultiple): Boolean { override fun areContentsTheSame(oldItem: DownloadItemConfigureMultiple, newItem: DownloadItemConfigureMultiple): Boolean {
return oldItem.title == newItem.title && return oldItem.title == newItem.title &&
oldItem.type == newItem.type && oldItem.type == newItem.type &&
oldItem.container == newItem.container &&
oldItem.videoPreferences == newItem.videoPreferences && oldItem.videoPreferences == newItem.videoPreferences &&
oldItem.format == newItem.format && oldItem.format == newItem.format &&
oldItem.incognito == newItem.incognito oldItem.incognito == newItem.incognito

View file

@ -27,7 +27,7 @@ import java.util.*
class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<ResultItem?, PlaylistAdapter.ViewHolder>(AsyncDifferConfig.Builder( class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<ResultItem?, PlaylistAdapter.ViewHolder>(AsyncDifferConfig.Builder(
DIFF_CALLBACK DIFF_CALLBACK
).build()) { ).build()) {
private val checkedItems: ArrayList<String> private val checkedItems: ArrayList<Long>
private val onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener
private val activity: Activity private val activity: Activity
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
@ -70,9 +70,9 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
// CHECKBOX ---------------------------------- // CHECKBOX ----------------------------------
val check = card.findViewById<CheckBox>(R.id.checkBox) val check = card.findViewById<CheckBox>(R.id.checkBox)
check.isChecked = checkedItems.contains(item.url) check.isChecked = checkedItems.contains(item.id)
check.setOnClickListener { check.setOnClickListener {
checkCard(check.isChecked, item.url) checkCard(check.isChecked, item.id)
} }
card.setOnClickListener { card.setOnClickListener {
@ -80,25 +80,25 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
} }
} }
private fun checkCard(isChecked: Boolean, itemURL: String) { private fun checkCard(isChecked: Boolean, id: Long) {
if (isChecked) { if (isChecked) {
checkedItems.add(itemURL) checkedItems.add(id)
} else { } else {
checkedItems.remove(itemURL) checkedItems.remove(id)
} }
onItemClickListener.onCardSelect(itemURL, isChecked, checkedItems) onItemClickListener.onCardSelect(id, isChecked, checkedItems)
} }
interface OnItemClickListener { interface OnItemClickListener {
fun onCardSelect(itemURL: String, isChecked: Boolean, checkedItems: List<String>) fun onCardSelect(itemID: Long, isChecked: Boolean, checkedItems: List<Long>)
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun clearCheckeditems() { fun clearCheckeditems() {
for (i in 0 until itemCount){ for (i in 0 until itemCount){
val item = getItem(i) val item = getItem(i)
if (checkedItems.find { it == item?.url } != null){ if (checkedItems.find { it == item?.id } != null){
checkedItems.remove(item?.url) checkedItems.remove(item?.id)
notifyItemChanged(i) notifyItemChanged(i)
} }
} }
@ -109,15 +109,15 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
checkedItems.clear() checkedItems.clear()
for (i in 0 until itemCount){ for (i in 0 until itemCount){
val item = getItem(i) val item = getItem(i)
checkedItems.add(item!!.url) checkedItems.add(item!!.id)
notifyItemChanged(i) notifyItemChanged(i)
} }
} }
fun invertSelected(items: List<ResultItem?>?){ fun invertSelected(items: List<ResultItem?>?){
val invertedList = mutableListOf<String>() val invertedList = mutableListOf<Long>()
items?.forEach { items?.forEach {
if (!checkedItems.contains(it!!.url)) invertedList.add(it.url) if (!checkedItems.contains(it!!.id)) invertedList.add(it.id)
} }
checkedItems.clear() checkedItems.clear()
checkedItems.addAll(invertedList) checkedItems.addAll(invertedList)
@ -128,19 +128,19 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
checkedItems.clear() checkedItems.clear()
if (start == end ){ if (start == end ){
val item = getItem(start) val item = getItem(start)
checkedItems.add(item!!.url) checkedItems.add(item!!.id)
notifyItemChanged(start) notifyItemChanged(start)
}else{ }else{
for (i in start..end){ for (i in start..end){
val item = getItem(i) val item = getItem(i)
checkedItems.add(item!!.url) checkedItems.add(item!!.id)
notifyItemChanged(i) notifyItemChanged(i)
} }
} }
} }
fun getCheckedItems() : List<String>{ fun getCheckedItems() : List<Long>{
return checkedItems return checkedItems
} }

View file

@ -16,12 +16,12 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.PopupMenu
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -90,6 +90,9 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var currentDownloadIDs: List<Long> private lateinit var currentDownloadIDs: List<Long>
private var processingItemsCount : Int = 0 private var processingItemsCount : Int = 0
private lateinit var containerBtn : MenuItem
private lateinit var containerTextView: TextView
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
@ -145,10 +148,16 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
itemTouchHelper.attachToRecyclerView(recyclerView) itemTouchHelper.attachToRecyclerView(recyclerView)
} }
scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button) scheduleBtn = view.findViewById(R.id.bottomsheet_schedule_button)
downloadBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_download_button) downloadBtn = view.findViewById(R.id.bottomsheet_download_button)
bottomAppBar = view.findViewById(R.id.bottomAppBar) bottomAppBar = view.findViewById(R.id.bottomAppBar)
val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type) val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type)
val formatBtn = bottomAppBar.menu.findItem(R.id.format)
val moreBtn = bottomAppBar.menu.findItem(R.id.more)
containerBtn = bottomAppBar.menu.findItem(R.id.container)
containerTextView = containerBtn.actionView as TextView
val incognitoBtn = bottomAppBar.menu.findItem(R.id.incognito)
filesize = view.findViewById(R.id.filesize) filesize = view.findViewById(R.id.filesize)
count = view.findViewById(R.id.count) count = view.findViewById(R.id.count)
@ -167,20 +176,28 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.processingDownloads.collectLatest { items -> downloadViewModel.processingDownloads.collectLatest { items ->
processingItemsCount = items.size processingItemsCount = items.size
count.text = "${processingItemsCount} ${getString(R.string.selected)}" count.text = "$processingItemsCount ${getString(R.string.selected)}"
listAdapter.submitList(items) listAdapter.submitList(items)
updateFileSize(items) updateFileSize(items)
if (items.isNotEmpty()){ if (items.isNotEmpty()){
if (items.all { it2 -> it2.type == items[0].type }) { if (items.all { it2 -> it2.type == items[0].type }) {
bottomAppBar.menu[1].icon?.alpha = 255 formatBtn.icon?.alpha = 255
if (items[0].type != DownloadViewModel.Type.command) { if (items[0].type != DownloadViewModel.Type.command) {
bottomAppBar.menu[4].icon?.alpha = 255 moreBtn.icon?.alpha = 255
} }
containerBtn.isVisible = items.first().type != DownloadViewModel.Type.command
} else { } else {
bottomAppBar.menu[1].icon?.alpha = 30 formatBtn.icon?.alpha = 30
bottomAppBar.menu[4].icon?.alpha = 30 moreBtn.icon?.alpha = 30
}
if (items.all { it.container == items[0].container }) {
setContainerText(items[0].container)
}else{
setContainerText("")
} }
val type = items.first().type val type = items.first().type
@ -191,10 +208,10 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
DownloadViewModel.Type.video -> { DownloadViewModel.Type.video -> {
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24) preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
} }
DownloadViewModel.Type.command -> { DownloadViewModel.Type.command -> {
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24) preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
setContainerText("")
} }
else -> {} else -> {}
@ -303,7 +320,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
downloadViewModel.areAllProcessingIncognito() downloadViewModel.areAllProcessingIncognito()
} }
bottomAppBar.menu.children.first { it.itemId == R.id.incognito }.icon!!.apply { incognitoBtn.icon!!.apply {
alpha = if (allIncognito){ alpha = if (allIncognito){
255 255
}else{ }else{
@ -336,7 +353,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
R.id.incognito -> { R.id.incognito -> {
lifecycleScope.launch { lifecycleScope.launch {
if (m.icon!!.alpha == 255) { if (m.icon!!.alpha == 255) {
bottomAppBar.menu[3].isEnabled = false incognitoBtn.isEnabled = false
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
downloadViewModel.updateProcessingIncognito(false) downloadViewModel.updateProcessingIncognito(false)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
@ -344,18 +361,18 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
m.isEnabled = true m.isEnabled = true
} }
} }
bottomAppBar.menu[3].icon?.alpha = 30 incognitoBtn.icon?.alpha = 30
bottomAppBar.menu[3].isEnabled = true incognitoBtn.isEnabled = true
Toast.makeText(requireContext(), "${getString(R.string.incognito)}: ${getString(R.string.disabled)}", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "${getString(R.string.incognito)}: ${getString(R.string.disabled)}", Toast.LENGTH_SHORT).show()
}else{ }else{
bottomAppBar.menu[3].isEnabled = false incognitoBtn.isEnabled = false
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
downloadViewModel.updateProcessingIncognito(true) downloadViewModel.updateProcessingIncognito(true)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
} }
} }
bottomAppBar.menu[3].icon?.alpha = 255 incognitoBtn.icon?.alpha = 255
bottomAppBar.menu[3].isEnabled = true incognitoBtn.isEnabled = true
Toast.makeText(requireContext(), "${getString(R.string.incognito)}: ${getString(R.string.ok)}", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "${getString(R.string.incognito)}: ${getString(R.string.ok)}", Toast.LENGTH_SHORT).show()
} }
} }
@ -393,10 +410,20 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
downloadViewModel.updateProcessingType(DownloadViewModel.Type.audio) downloadViewModel.updateProcessingType(DownloadViewModel.Type.audio)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24) preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
bottomAppBar.menu[1].icon?.alpha = 255 formatBtn.icon?.alpha = 255
bottomAppBar.menu[4].icon?.alpha = 255 moreBtn.icon?.alpha = 255
bottomSheet.cancel() bottomSheet.cancel()
} }
val res = downloadViewModel.checkIfAllProcessingItemsHaveSameContainer()
withContext(Dispatchers.Main) {
if (!res.first) {
setContainerText("")
}else{
setContainerText(res.second)
}
}
} }
} }
@ -405,10 +432,19 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
downloadViewModel.updateProcessingType(DownloadViewModel.Type.video) downloadViewModel.updateProcessingType(DownloadViewModel.Type.video)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24) preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
bottomAppBar.menu[1].icon?.alpha = 255 formatBtn.icon?.alpha = 255
bottomAppBar.menu[4].icon?.alpha = 255 moreBtn.icon?.alpha = 255
bottomSheet.cancel() bottomSheet.cancel()
} }
val res = downloadViewModel.checkIfAllProcessingItemsHaveSameContainer()
withContext(Dispatchers.Main) {
if (!res.first) {
setContainerText("")
}else{
setContainerText(res.second)
}
}
} }
} }
@ -417,10 +453,12 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
downloadViewModel.updateProcessingType(DownloadViewModel.Type.command) downloadViewModel.updateProcessingType(DownloadViewModel.Type.command)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24) preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
bottomAppBar.menu[1].icon?.alpha = 255 formatBtn.icon?.alpha = 255
bottomAppBar.menu[4].icon?.alpha = 30 moreBtn.icon?.alpha = 30
bottomSheet.cancel() bottomSheet.cancel()
containerBtn.isVisible = false
} }
} }
} }
@ -676,6 +714,41 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
true true
} }
containerTextView.setOnClickListener {
lifecycleScope.launch {
val res = withContext(Dispatchers.IO){
downloadViewModel.checkIfAllProcessingItemsHaveSameType()
}
if (!res.first){
Toast.makeText(requireContext(), getString(R.string.format_filtering_hint), Toast.LENGTH_SHORT).show()
}else{
val popup = PopupMenu(activity, containerTextView)
when(res.second) {
DownloadViewModel.Type.audio -> resources.getStringArray(R.array.audio_containers)
//video
else -> resources.getStringArray(R.array.video_containers)
}.forEach {
popup.menu.add(it)
}
popup.setOnMenuItemClickListener { mm ->
val container = mm.title
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.updateProcessingContainer(container.toString())
}
}
setContainerText(container.toString())
true
}
popup.show()
}
}
}
} }
private fun toggleLoading(loading: Boolean){ private fun toggleLoading(loading: Boolean){
@ -964,5 +1037,21 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
} }
@SuppressLint("SetTextI18n")
private fun setContainerText(cont: String?) {
if (cont == null) {
containerBtn.isVisible = false
return
}
if (cont == getString(R.string.defaultValue) || cont.isEmpty()) {
containerTextView.text = ".ext"
return
}else{
containerTextView.text = cont
}
containerBtn.isVisible = true
}
} }

View file

@ -55,7 +55,6 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
private lateinit var selectBetween: MenuItem private lateinit var selectBetween: MenuItem
private lateinit var resultItemIDs: List<Long> private lateinit var resultItemIDs: List<Long>
private lateinit var itemURLs: List<String>
private var items = listOf<ResultItem>() private var items = listOf<ResultItem>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -179,9 +178,9 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
ok.isEnabled = false ok.isEnabled = false
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val checkedItems = listAdapter.getCheckedItems() val checkedItems = listAdapter.getCheckedItems()
val checkedResultItems = items.filter { item -> checkedItems.contains(item.url) } val checkedResultItems = items.filter { item -> checkedItems.contains(item.id) }
if (checkedResultItems.size == 1){ if (checkedResultItems.size == 1){
val resultItem = resultViewModel.getItemByURL(checkedResultItems[0].url)!! val resultItem = resultViewModel.getByID(checkedResultItems[0].id)!!
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf( findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf(
Pair("result", resultItem), Pair("result", resultItem),
@ -223,8 +222,8 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
if(selectedItems.size != 2){ if(selectedItems.size != 2){
m.isVisible = false m.isVisible = false
}else{ }else{
val item2 = itemURLs.indexOf(selectedItems.last()) val item2 = resultItemIDs.indexOf(selectedItems.last())
val item1 = itemURLs.indexOf(selectedItems.first()) val item1 = resultItemIDs.indexOf(selectedItems.first())
if(item1 > item2) listAdapter.checkRange(item2, item1) if(item1 > item2) listAdapter.checkRange(item2, item1)
else listAdapter.checkRange(item1, item2) else listAdapter.checkRange(item1, item2)
count.text = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}" count.text = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}"
@ -249,7 +248,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
toTextInput.isEnabled = !isLoading toTextInput.isEnabled = !isLoading
items = it items = it
itemURLs = items.map { itm -> itm.url } resultItemIDs = items.map { itm -> itm.id }
} }
} }
@ -284,7 +283,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
} }
override fun onCardSelect(itemURL: String, isChecked: Boolean, checkedItems: List<String>) { override fun onCardSelect(itemID: Long, isChecked: Boolean, checkedItems: List<Long>) {
if (checkedItems.size == items.size){ if (checkedItems.size == items.size){
count.text = "(${listAdapter.getCheckedItems().size}) ${resources.getString(R.string.all_items_selected)} " count.text = "(${listAdapter.getCheckedItems().size}) ${resources.getString(R.string.all_items_selected)} "
}else{ }else{
@ -302,8 +301,8 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
val size = checkedItems.size val size = checkedItems.size
if(size != 2) false if(size != 2) false
else { else {
val item1 = itemURLs.indexOf(checkedItems.first()) val item1 = resultItemIDs.indexOf(checkedItems.first())
val item2 = itemURLs.indexOf(checkedItems.last()) val item2 = resultItemIDs.indexOf(checkedItems.last())
(item1-item2).absoluteValue > 1 (item1-item2).absoluteValue > 1
} }

View file

@ -167,12 +167,17 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
} }
lifecycleScope.launch { lifecycleScope.launch {
val activeQueuedDownloadsCount = withContext(Dispatchers.IO) { val activeCount = withContext(Dispatchers.IO) {
downloadViewModel.getActiveQueuedDownloadsCount() downloadViewModel.getActiveDownloadsCount()
} }
val queuedCount = withContext(Dispatchers.IO) {
downloadViewModel.getQueuedDownloadsCount()
}
val pausedDownloads = preferences.getBoolean("paused_downloads", false) val pausedDownloads = preferences.getBoolean("paused_downloads", false)
pause.isVisible = activeQueuedDownloadsCount > 0 && !pausedDownloads pause.isVisible = (queuedCount > 0 || activeCount > 0) && !pausedDownloads
resume.isVisible = activeQueuedDownloadsCount > 0 && pausedDownloads resume.isVisible = (queuedCount > 0 || activeCount > 0) && pausedDownloads
} }
lifecycleScope.launch { lifecycleScope.launch {

View file

@ -15,6 +15,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.WorkManager
import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
@ -22,11 +23,14 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.more.settings.SettingsActivity import com.deniscerri.ytdl.ui.more.settings.SettingsActivity
import com.deniscerri.ytdl.ui.more.terminal.TerminalActivity import com.deniscerri.ytdl.ui.more.terminal.TerminalActivity
import com.deniscerri.ytdl.util.NavbarUtil import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.system.exitProcess import kotlin.system.exitProcess
class MoreFragment : Fragment() { class MoreFragment : Fragment() {
@ -125,28 +129,40 @@ class MoreFragment : Fragment() {
doNotShowAgain = compoundButton.isChecked doNotShowAgain = compoundButton.isChecked
} }
val workManager = WorkManager.getInstance(requireContext())
val notificationUtil = NotificationUtil(requireContext())
terminateDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } terminateDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
terminateDialog.setPositiveButton(getString(R.string.ok)) { diag: DialogInterface?, _: Int -> terminateDialog.setPositiveButton(getString(R.string.ok)) { diag: DialogInterface?, _: Int ->
runBlocking { lifecycleScope.launch {
val job : Job = lifecycleScope.launch(Dispatchers.IO) { val activeDownloads = withContext(Dispatchers.IO){
val activeDownloads = downloadViewModel.getActiveDownloads().toMutableList() downloadViewModel.getActiveDownloadsCount()
activeDownloads.map { it.status = DownloadRepository.Status.Queued.toString() }
activeDownloads.forEach {
downloadViewModel.updateDownload(it)
}
} }
runBlocking { if (activeDownloads > 0) {
job.join() workManager.cancelAllWorkByTag("download")
if (doNotShowAgain){ val activeDownloadsList = withContext(Dispatchers.IO){
mainSharedPreferencesEditor.putBoolean("ask_terminate_app", false) downloadViewModel.getActiveDownloads()
mainSharedPreferencesEditor.commit()
} }
mainActivity.finishAndRemoveTask()
mainActivity.finishAffinity()
exitProcess(0)
}
}
activeDownloadsList.forEach {
it.status = DownloadRepository.Status.Queued.toString()
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt())
withContext(Dispatchers.IO) {
downloadViewModel.updateDownload(it)
}
}
mainSharedPreferencesEditor.putBoolean("paused_downloads", true).apply()
}
if (doNotShowAgain){
mainSharedPreferencesEditor.putBoolean("ask_terminate_app", false).apply()
}
mainSharedPreferencesEditor.commit()
mainActivity.finishAndRemoveTask()
mainActivity.finishAffinity()
exitProcess(0)
}
} }
terminateDialog.show() terminateDialog.show()
}else{ }else{

View file

@ -20,10 +20,12 @@ import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.LogViewModel import com.deniscerri.ytdl.database.viewmodel.LogViewModel
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.enableTextHighlight import com.deniscerri.ytdl.util.Extensions.enableTextHighlight
import com.deniscerri.ytdl.util.Extensions.setCustomTextSize import com.deniscerri.ytdl.util.Extensions.setCustomTextSize
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
@ -104,8 +106,14 @@ class DownloadLogFragment : Fragment() {
} }
} }
content.isFocusable = true lifecycleScope.launch(Dispatchers.IO){
content.enableTextHighlight() content.isFocusable = true
content.enableTextHighlight()
}
contentScrollView.enableFastScroll()
val slider = view.findViewById<Slider>(R.id.textsize_seekbar) val slider = view.findViewById<Slider>(R.id.textsize_seekbar)
bottomAppBar?.setOnMenuItemClickListener { m: MenuItem -> bottomAppBar?.setOnMenuItemClickListener { m: MenuItem ->
when(m.itemId){ when(m.itemId){

View file

@ -28,6 +28,7 @@ import android.view.ViewOutlineProvider
import android.view.animation.Interpolator import android.view.animation.Interpolator
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.Px import androidx.annotation.Px
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -139,6 +140,15 @@ object Extensions {
.build() .build()
} }
fun ScrollView.enableFastScroll() {
val drawable = ShapeDrawable(OvalShape())
drawable.paint.color = context.getColor(android.R.color.transparent)
FastScrollerBuilder(this)
.useMd2Style()
.setTrackDrawable(drawable)
.build()
}
fun File.getMediaDuration(context: Context): Int { fun File.getMediaDuration(context: Context): Int {
return kotlin.runCatching { return kotlin.runCatching {
if (!exists()) return 0 if (!exists()) return 0
@ -251,20 +261,20 @@ object Extensions {
} }
} }
fun String.appendLineToLog(line: String): String { // fun String.appendLineToLog(line: String): String {
val lines = this.split("\n") // val lines = this.split("\n")
if (!lines.takeLast(3).contains(line)){ // if (!lines.takeLast(3).contains(line)){
//clean dublicate progress + add newline // //clean dublicate progress + add newline
var newLine = line // var newLine = line
if (newLine.contains("[download")) { // if (newLine.contains("[download")) {
newLine = "[download]" + line.split("[download]").last() // newLine = "[download]" + line.split("[download]").last()
} // }
//
return lines.dropLastWhile { it.contains("[download") }.joinToString("\n") + "\n${newLine}" // return lines.dropLastWhile { it.contains("[download") }.joinToString("\n") + "\n${newLine}"
} // }
//
return this // return this
} // }
fun ImageView.loadThumbnail(hideThumb: Boolean, imageURL: String){ fun ImageView.loadThumbnail(hideThumb: Boolean, imageURL: String){
if(!hideThumb){ if(!hideThumb){

View file

@ -97,11 +97,15 @@ class DownloadWorker(
val running = ArrayList(runningYTDLInstances) val running = ArrayList(runningYTDLInstances)
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (items.isEmpty() && running.isEmpty()) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) if (items.isEmpty() && running.isEmpty()) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
return@collect
}
if (useScheduler){ if (useScheduler){
if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) { if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
return@collect
} }
} }
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size
@ -138,15 +142,16 @@ class DownloadWorker(
val commandString = infoUtil.parseYTDLRequestString(request) val commandString = infoUtil.parseYTDLRequestString(request)
val logString = StringBuilder("\n ${commandString}\n\n") val initialLogDetails = "Downloading:\n" +
"Title: ${downloadItem.title}\n" +
"URL: ${downloadItem.url}\n" +
"Type: ${downloadItem.type}\n" +
"Command: \n $commandString \n\n"
val logString = StringBuilder(initialLogDetails)
val logItem = LogItem( val logItem = LogItem(
0, 0,
downloadItem.title.ifBlank { downloadItem.url }, downloadItem.title.ifBlank { downloadItem.url },
"Downloading:\n" + logString.toString(),
"Title: ${downloadItem.title}\n" +
"URL: ${downloadItem.url}\n" +
"Type: ${downloadItem.type}\n" +
"Command: $logString",
downloadItem.format, downloadItem.format,
downloadItem.type, downloadItem.type,
System.currentTimeMillis(), System.currentTimeMillis(),
@ -182,7 +187,7 @@ class DownloadWorker(
resultRepo.updateDownloadItem(downloadItem)?.apply { resultRepo.updateDownloadItem(downloadItem)?.apply {
dao.updateWithoutUpsert(this) dao.updateWithoutUpsert(this)
} }
val wasQuickDownloaded = resultDao.getCountInt() == 0 //val wasQuickDownloaded = resultDao.getCountInt() == 0
runBlocking { runBlocking {
var finalPaths : MutableList<String>? var finalPaths : MutableList<String>?
@ -304,7 +309,7 @@ class DownloadWorker(
dao.delete(downloadItem.id) dao.delete(downloadItem.id)
if (logDownloads){ if (logDownloads){
logRepo.update(it.out, logItem.id) logRepo.update(initialLogDetails + "\n" + it.out, logItem.id, true)
} }
} }

View file

@ -88,11 +88,12 @@ class TerminalDownloadWorker(
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false) val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
val initialLogDetails = "Terminal Task\n" +
"Command: \n ${command}\n\n"
val logItem = LogItem( val logItem = LogItem(
0, 0,
"Terminal Task", "Terminal Task",
"Terminal Task\n" + initialLogDetails,
"Command: \n ${command}\n\n",
Format(), Format(),
DownloadViewModel.Type.command, DownloadViewModel.Type.command,
System.currentTimeMillis(), System.currentTimeMillis(),
@ -139,8 +140,7 @@ class TerminalDownloadWorker(
} }
} }
} }
if (logDownloads) logRepo.update(initialLogDetails + "\n" + it.out, logItem.id, true)
if (logDownloads) logRepo.update(it.out, logItem.id)
dao.updateLog(it.out, itemId.toLong()) dao.updateLog(it.out, itemId.toLong())
notificationUtil.cancelDownloadNotification(itemId) notificationUtil.cancelDownloadNotification(itemId)
delay(1000) delay(1000)

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.Material3.Button.TextButton"
android:id="@+id/bottom_app_bar_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="true"
android:minHeight="0dp"
android:minWidth="0dp"
android:textColor="?android:colorAccent"
android:textStyle="bold"
android:textSize="15sp"/>

View file

@ -187,6 +187,27 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/container"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/codec" android:id="@+id/codec"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary" style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"

View file

@ -38,7 +38,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ScrollView <me.zhanghai.android.fastscroll.FastScrollScrollView
android:id="@+id/content_scrollview" android:id="@+id/content_scrollview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -65,7 +65,7 @@
</LinearLayout> </LinearLayout>
</ScrollView> </me.zhanghai.android.fastscroll.FastScrollScrollView>
</FrameLayout> </FrameLayout>

View file

@ -22,6 +22,12 @@
android:icon="@drawable/baseline_folder_24" android:icon="@drawable/baseline_folder_24"
app:showAsAction="always"/> app:showAsAction="always"/>
<item
android:id="@+id/container"
android:title="@string/container"
app:actionLayout="@layout/bottom_app_bar_text"
app:showAsAction="always"/>
<item <item
android:id="@+id/incognito" android:id="@+id/incognito"
android:title="@string/incognito" android:title="@string/incognito"

View file

@ -149,7 +149,7 @@
android:defaultValue="" android:defaultValue=""
android:entries="@array/cleanup_leftover_downloads" android:entries="@array/cleanup_leftover_downloads"
android:entryValues="@array/cleanup_leftover_downloads_values" android:entryValues="@array/cleanup_leftover_downloads_values"
android:icon="@drawable/baseline_delete_24" android:icon="@drawable/ic_folder_delete"
app:key="cleanup_leftover_downloads" app:key="cleanup_leftover_downloads"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:title="@string/cleanup_leftover_downloads" /> app:title="@string/cleanup_leftover_downloads" />

View file

@ -1,87 +1,43 @@
TEMPORARY just to save changelog # What's Changed
List is empty. - Fix App crashing with exception "com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.checkIfAllProcessingItemsHaveSameType(Unknown Source:21)..."
- Added option in download settings to use AlarmManager instead of WorkManager for scheduled downloads to improve accuracy
kotlin.collections.CollectionsKt.first(SourceFile:2) - When updating formats for multiple items, now the app remembers what items don't need new formats and skips them to save time and data
com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.checkIfAllProcessingItemsHaveSameType(Unknown Source:21) - Moved format source from the bottom of the format list to the filter bottom sheet
com.deniscerri.ytdl.ui.downloadcard.DownloadMultipleBottomSheetDialog$setupDialog$9$3$res$1.invokeSuspend(SourceFile:18) - Added ability for the app to update the result items formats when you update the download item formats if there are any results with the same url
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(SourceFile:9) - Moved "In Queue" screen back in its separate tab due to recyclerview performance issues
kotlinx.coroutines.DispatchedTask.run(Unknown Source:96) - Format updating in multiple download card now applies updates immediately on each item
androidx.work.Worker$2.run(SourceFile:39) - Deleted saved downloads after a format update in the background after the user decided to open for review
kotlinx.coroutines.scheduling.TaskImpl.run(Unknown Source:2) - App now shows the actual predicted filesize along with the audio size when downloading videos in the multiple download card. Also in single download mode when the audio size is known but the video size is unknown the app wont show the audio size only, it wont show anything
kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(SourceFile:96) - Made snackbar show up above the copy log button
- Fixed App crashing when updating fragments in the download card while in the Share Activity
- Added a setting to use the video url instead of the playlist url to download. The app used playlist url and then indexes to get the video to take advantage of playlist metadata. Some users have been using libretube playlists that cant be recognised properly by yt-dlp. Turn this option if this affects you
add option in download settings to use alarm manager instead of workmanager for scheduled tasks - Added Write URI Permission when opening files
- Fixed App still updating formats even though the user closed the format bottom sheet
fix audio format importance - Fixed App keeping old video titles in the download card #526
- Refactored code on how to fetch player url and chapters from yt-dlp in the cut bottom sheet
When updating formats for multiple items, now the app remembers what items dont need new formats and skips them - Added ability to change the theme accent without restarting the app. If you change light/dark mode the app is supposed to restart because the icon activity changes aswell
- Prevented AVI and FLV containers from embedding thumbnails
Move format source from the bottom of the format list to the filter bottom sheet - Fixed Subs Language preference not being saved in the download card
- Added player_client=default,mediaconnect,android extractor args when data fetching and downloading to use more formats
Added ability for the app to update the result items formats when you update the download item formats if there are any results with the same url - Turned some metadata into digits
moved queue screen back in its separate tab due to performance issues
format updating in the multiple download card now applies updates immediately on each item
Update formats in background review. Also prevent from showing up every time it updates. Also delete saved after opening items n the multiple download card lol wtf. Also the notification. Change the title of notification to
Can that other one under the Copy log be fixed, too?snackbar
Bulk download shows size without audio fixed
When selecting audio format add generic last
Update ui crashes app from share activity
add option to use video url instead of playlist url to avoid piped private playlist issue
add write uri permission when opening files
fix app still updating formats even though the user closed the format bottom sheet
[BUG] Video title doesn't change in the download card #526
fixed app fetching player urls and chapters from yt-dlp
added ability to change theme without restarting the app. If the icon changes then the app will restart
prevent avi and flv containers from embedding thumbnails
Fixed Subs language preference isn't saved in the download card
add player_client=default,mediaconnect,android extractor args when data fetching and downloading to show more formats
Turned some metadata into digits
-> --parse-metadata "%(track_number,playlist_index)d:(?P<track_number>\d+)" --parse-metadata "%(dscrptn_year,release_year,release_date>%Y,upload_date>%Y)s:(?P<meta_date>\d+)" -> --parse-metadata "%(track_number,playlist_index)d:(?P<track_number>\d+)" --parse-metadata "%(dscrptn_year,release_year,release_date>%Y,upload_date>%Y)s:(?P<meta_date>\d+)"
Removed NA from meta album artist --parse-metadata "%(artist,uploader|)s:^(?P<meta_album_artist>[^,]*)" - Removed NA from meta album artist --parse-metadata "%(artist,uploader|)s:^(?P<meta_album_artist>[^,]*)"
- Added --recode-video toggle in the settings to use instead of --merge-output-format
Add --recode-video toggle in the settings to use instead of --merge-output-format - Also added that preference as a button in the adjust video in the download card
- Adjusted the "Adjust video" section in the download card and bundled some common items together to save space
adjusted the "Adjust video" section in the download card and put all subtitle items together - Fixed app not selecting an audio format to show in the format view in the download card after updating formats
- Fixed app showing when the download will start up when you queue up a scheduled download
Fixed app not selecting an audio format to show in the format view in the download card after updating formats - Made home recyclerview bottom padding bigger to avoid the FABs
- Fixed Issue #529
download_rescheduled_to shows up twice, one in original language and one in english - App now remembers the scroll position in the download history tab
- Made piped api not fetch data when fed mix playlists
It's very difficult to tap on video icon for the video at the bottom - Turned the incognito toast to a snackbar in the download card
- Fixed duplicate dialog crashing app when in share activity
#529 - Fixed issue of app not showing audio formats of preferred language at the top when selecting the suggested filter
- Added a feature to delete cancelled downloads, errored downloads and download cache older than a certain period. You can select daily, weekly, monthly.
https://www.instagram.com/reel/C9zTRlONU6G/?igsh=MWJ1djBhYjdscG5wMw== chooses smallest format - Added ability to change the container in the multiple download card
- Fixed App issues with download logs
Limit audio id length - Added preferred language in the format sorter in yt-dlp
- More other small changes i forgor what i fixed
Remember scroll position in history tab
Make piped not fetch mix playlists
turn incognito toast to snackbar
duplicate dialog crash app when in share activity because the activity closes too early
4. In videos with multiple languages, previously, when I entered the suggested tab, all audios in my preferred language were displayed at the top, with the lowest quality. However, this no longer happens, and now I need to search among multiple languages (again, I'm not entirely sure about this, but it's challenging to compare with a previous version since I no longer have access to older app versions)