more fixes and features

This commit is contained in:
zaednasr 2024-07-27 20:32:21 +02:00
parent f4f527721c
commit b45ddbeffc
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
33 changed files with 437 additions and 116 deletions

View file

@ -23,6 +23,7 @@ import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.os.bundleOf
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach
import androidx.core.view.isVisible
@ -63,9 +64,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader

View file

@ -170,6 +170,9 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE status='Processing'")
suspend fun deleteProcessing()
@Query("DELETE FROM downloads WHERE status='Duplicate'")
suspend fun deleteWithDuplicateStatus()
@Query("DELETE FROM downloads WHERE status='Scheduled'")
suspend fun deleteScheduled()

View file

@ -174,6 +174,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
deleteCache(cancelled)
}
fun getActiveDownloadsCount() : Int {
return downloadDao.getDownloadsCountByStatus(listOf(Status.Active).toListString())
}
suspend fun deleteScheduled() {
downloadDao.deleteScheduled()
}
@ -196,6 +200,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.deleteProcessing()
}
suspend fun deleteWithDuplicateStatus() {
downloadDao.deleteWithDuplicateStatus()
}
suspend fun deleteAllWithIDs(ids: List<Long>){
downloadDao.deleteAllWithIDs(ids)
@ -214,7 +222,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
}
@SuppressLint("RestrictedApi")
suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) {
suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) : Result<String> {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workManager = WorkManager.getInstance(context)
@ -236,7 +244,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
if (delay > 0L && useAlarmForScheduling) {
AlarmScheduler(context).scheduleAt(queuedItems.minBy { it.downloadStartTime }.downloadStartTime)
return
return Result.success("")
}
@ -256,26 +264,22 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
)
}
val handler = Handler(Looper.getMainLooper())
val message = StringBuilder()
val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered
if (!allowMeteredNetworks && isCurrentNetworkMetered){
handler.postDelayed({
Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show()
}, 0)
message.appendLine(context.getString(R.string.metered_network_download_start_info))
}
if (queuedItems.isNotEmpty()) {
val first = queuedItems.first()
if (first.downloadStartTime > 0L) {
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(queuedItems.first().downloadStartTime)
handler.postDelayed({
Toast.makeText(context, context.getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
}, 0)
message.appendLine(context.getString(R.string.download_rescheduled_to) + " " + date)
}
}
return Result.success(message.toString())
}
}

View file

@ -638,6 +638,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.deleteProcessing()
}
fun deleteWithDuplicateStatus() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteWithDuplicateStatus()
}
suspend fun deleteAllWithID(ids: List<Long>) {
repository.deleteAllWithIDs(ids)
}
@ -671,7 +675,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun getActiveDownloadsCount() : Int {
return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Active).toListString())
return repository.getActiveDownloadsCount()
}
fun getActiveQueuedDownloadsCount() : Int {
@ -761,12 +765,17 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.startDownloadWorker(emptyList(), application)
}
suspend fun queueProcessingDownloads(){
suspend fun queueProcessingDownloads() : QueueDownloadsResult {
val processingItems = repository.getProcessingDownloads()
queueDownloads(processingItems)
return queueDownloads(processingItems)
}
suspend fun queueDownloads(items: List<DownloadItem>, ignoreDuplicates : Boolean = false) {
data class QueueDownloadsResult(
var message: String,
var duplicateDownloadIDs : List<AlreadyExistsIDs>
)
suspend fun queueDownloads(items: List<DownloadItem>, ignoreDuplicates : Boolean = false) : QueueDownloadsResult {
val context = App.instance
val alarmScheduler = AlarmScheduler(context)
val queuedItems = mutableListOf<DownloadItem>()
@ -788,10 +797,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
items.forEach {
if (it.status != DownloadRepository.Status.Scheduled.toString()) {
if (it.downloadStartTime > 0) {
it.status = DownloadRepository.Status.Scheduled.toString()
}else{
it.status = DownloadRepository.Status.Queued.toString()
}
//CHECK DUPLICATES
var isDuplicate = false
if (checkDuplicate.isNotEmpty() && !ignoreDuplicates){
@ -903,22 +915,23 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
val queued = repository.updateAll(queuedItems)
val result = QueueDownloadsResult("", listOf())
//if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
if (alarmScheduler.canSchedule()){
repository.updateAll(queuedItems)
alarmScheduler.schedule()
}else{
sharedPreferences.edit().putBoolean("use_scheduler", false).apply()
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, context.getString(R.string.enable_alarm_permission), Toast.LENGTH_LONG).show()
}
result.message = context.getString(R.string.enable_alarm_permission)
}
}else{
val queued = repository.updateAll(queuedItems)
if (!sharedPreferences.getBoolean("paused_downloads", false)) {
repository.startDownloadWorker(queued, context)
result.message = repository.startDownloadWorker(queued, context).getOrElse { "" }
}
if(!useScheduler){
@ -947,7 +960,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
if (existingItemIDs.isNotEmpty()){
alreadyExistsUiState.value = existingItemIDs.toList()
result.duplicateDownloadIDs = existingItemIDs.toList()
}
return result
}
fun getQueuedCollectedFileSize() : Long {
@ -1077,13 +1093,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
}
suspend fun updateProcessingDownloadTimeAndQueueScheduled(time: Long) {
suspend fun updateProcessingDownloadTimeAndQueueScheduled(time: Long) : QueueDownloadsResult {
val processing = repository.getProcessingDownloads()
processing.forEach {
it.downloadStartTime = time
it.status = DownloadRepository.Status.Scheduled.toString()
}
queueDownloads(processing)
return queueDownloads(processing)
}
fun checkIfAllProcessingItemsHaveSameType() : Pair<Boolean, Type> {

View file

@ -213,18 +213,6 @@ class ShareActivity : BaseActivity() {
}
this@ShareActivity.finish()
}
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.isNotEmpty()){
withContext(Dispatchers.Main){
val bundle = bundleOf(
Pair("duplicates", res)
)
navController.navigate(R.id.downloadsAlreadyExistDialog2, bundle)
}
downloadViewModel.alreadyExistsUiState.value = mutableListOf()
}
}
}
}
}

View file

@ -11,10 +11,19 @@ import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.ThemeUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.system.exitProcess
open class BaseActivity : AppCompatActivity() {

View file

@ -221,7 +221,7 @@ class ConfigureDownloadBottomSheetDialog(private val currentDownloadItem: Downlo
incognito = !incognito
fragmentAdapter.isIncognito = incognito
val onOff = if (incognito) getString(R.string.ok) else getString(R.string.disabled)
Toast.makeText(requireContext(), "${getString(R.string.incognito)}: $onOff", Toast.LENGTH_SHORT).show()
Snackbar.make(incognitoBtn, "${getString(R.string.incognito)}: $onOff", Snackbar.LENGTH_SHORT).show()
}

View file

@ -39,6 +39,7 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.receiver.ShareActivity
import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout
@ -80,6 +81,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var title : View
private lateinit var shimmerLoadingSubtitle : ShimmerFrameLayout
private lateinit var subtitle : View
private lateinit var parentActivity: BaseActivity
private lateinit var result: ResultItem
@ -95,7 +97,6 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
infoUtil = InfoUtil(requireContext())
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val res: ResultItem?
val dwl: DownloadItem?
@ -131,6 +132,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
view = LayoutInflater.from(context).inflate(R.layout.download_bottom_sheet, null)
dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
parentActivity = activity as BaseActivity
dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View)
@ -331,21 +333,34 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
audioDownloadItem.status = DownloadRepository.Status.Scheduled.toString()
itemsToQueue.add(audioDownloadItem)
runBlocking {
downloadViewModel.queueDownloads(itemsToQueue)
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(item.downloadStartTime)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
lifecycleScope.launch {
val result = withContext(Dispatchers.IO){
downloadViewModel.queueDownloads(itemsToQueue)
}
if (result.message.isNotBlank()){
Toast.makeText(requireContext(), result.message, Toast.LENGTH_LONG).show()
}
withContext(Dispatchers.Main){
handleDuplicatesAndDismiss(result.duplicateDownloadIDs)
}
}
dismiss()
}
}else{
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(item.downloadStartTime)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
}
dismiss()
lifecycleScope.launch {
val result = withContext(Dispatchers.IO){
downloadViewModel.queueDownloads(listOf(item))
}
if (result.message.isNotBlank()){
Toast.makeText(requireContext(), result.message, Toast.LENGTH_LONG).show()
}
withContext(Dispatchers.Main){
handleDuplicatesAndDismiss(result.duplicateDownloadIDs)
}
}
}
}
@ -365,15 +380,17 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
itemsToQueue.add(it)
runBlocking {
downloadViewModel.queueDownloads(itemsToQueue)
val result = downloadViewModel.queueDownloads(itemsToQueue)
withContext(Dispatchers.Main){
handleDuplicatesAndDismiss(result.duplicateDownloadIDs)
}
}
dismiss()
}
}else{
runBlocking {
val result = withContext(Dispatchers.IO) {
downloadViewModel.queueDownloads(listOf(item))
}
dismiss()
handleDuplicatesAndDismiss(result.duplicateDownloadIDs)
}
}
}
@ -441,7 +458,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
incognito = !incognito
fragmentAdapter.isIncognito = incognito
val onOff = if (incognito) getString(R.string.ok) else getString(R.string.disabled)
Toast.makeText(requireContext(), "${getString(R.string.incognito)}: $onOff", Toast.LENGTH_SHORT).show()
Snackbar.make(incognitoBtn, "${getString(R.string.incognito)}: $onOff", Snackbar.LENGTH_SHORT).show()
}
@ -584,6 +601,23 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
}
}
lifecycleScope.launch {
launch{
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.isNotEmpty() && activity is ShareActivity){
withContext(Dispatchers.Main){
val bundle = bundleOf(
Pair("duplicates", ArrayList(res))
)
delay(500)
findNavController().navigate(R.id.action_downloadBottomSheetDialog_to_downloadsAlreadyExistDialog2, bundle)
}
downloadViewModel.alreadyExistsUiState.value = mutableListOf()
}
}
}
}
lifecycleScope.launch {
resultViewModel.updateFormatsResultData.collectLatest { formats ->
if (formats == null) return@collectLatest
@ -686,5 +720,13 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
super.onDismiss(dialog)
}
}
private fun handleDuplicatesAndDismiss(res: List<DownloadViewModel.AlreadyExistsIDs>) {
if (activity is ShareActivity && res.isNotEmpty()) {
//let the lifecycle listener handle it
}else{
dismiss()
}
}
}

View file

@ -19,11 +19,13 @@ import android.view.Window
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
@ -37,6 +39,8 @@ import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.receiver.ShareActivity
import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.ui.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.FileUtil
@ -56,6 +60,7 @@ import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -80,6 +85,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var shimmerTitle: ShimmerFrameLayout
private lateinit var shimmerSubtitle: ShimmerFrameLayout
private lateinit var sharedPreferences: SharedPreferences
private lateinit var parentActivity: BaseActivity
private lateinit var currentDownloadIDs: List<Long>
private var processingItemsCount : Int = 0
@ -103,6 +109,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val view = LayoutInflater.from(context).inflate(R.layout.download_multiple_bottom_sheet, null)
dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
parentActivity = activity as BaseActivity
dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View)
@ -204,9 +211,20 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.updateProcessingDownloadTimeAndQueueScheduled(cal.timeInMillis)
val result = downloadViewModel.updateProcessingDownloadTimeAndQueueScheduled(cal.timeInMillis)
if (result.message.isNotBlank()){
lifecycleScope.launch {
withContext(Dispatchers.Main) {
Toast.makeText(requireContext(), result.message, Toast.LENGTH_LONG).show()
}
}
}
withContext(Dispatchers.Main){
handleDuplicatesAndDismiss(result.duplicateDownloadIDs)
dismiss()
}
}
dismiss()
}
}
}
@ -216,9 +234,20 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.queueProcessingDownloads()
val result = downloadViewModel.queueProcessingDownloads()
if (result.message.isNotBlank()){
lifecycleScope.launch {
withContext(Dispatchers.Main) {
Toast.makeText(requireContext(), result.message, Toast.LENGTH_LONG).show()
}
}
}
withContext(Dispatchers.Main){
handleDuplicatesAndDismiss(result.duplicateDownloadIDs)
dismiss()
}
}
dismiss()
}
}
@ -284,6 +313,23 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
lifecycleScope.launch {
launch{
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.isNotEmpty() && activity is ShareActivity){
withContext(Dispatchers.Main){
val bundle = bundleOf(
Pair("duplicates", ArrayList(res))
)
delay(500)
findNavController().navigate(R.id.action_downloadMultipleBottomSheetDialog_to_downloadsAlreadyExistDialog2, bundle)
}
downloadViewModel.alreadyExistsUiState.value = mutableListOf()
}
}
}
}
bottomAppBar.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) {
@ -910,5 +956,13 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
}
private fun handleDuplicatesAndDismiss(res: List<DownloadViewModel.AlreadyExistsIDs>) {
if (activity is ShareActivity && res.isNotEmpty()) {
//let the lifecycle listener handle it
}else{
dismiss()
}
}
}

View file

@ -10,6 +10,7 @@ import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.View
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
@ -28,6 +29,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -102,10 +104,17 @@ class DownloadsAlreadyExistDialog : BottomSheetDialogFragment(), AlreadyExistsAd
view.findViewById<MaterialButton>(R.id.bottomsheet_download_button).setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteProcessing()
downloadViewModel.deleteWithDuplicateStatus()
val items = duplicates.map { it.downloadItem }
items.forEach { it.id = 0 }
downloadViewModel.queueDownloads(items, true)
val result = downloadViewModel.queueDownloads(items, true)
if (result.message.isNotBlank()){
lifecycleScope.launch {
withContext(Dispatchers.Main) {
Toast.makeText(requireContext(), result.message, Toast.LENGTH_LONG).show()
}
}
}
withContext(Dispatchers.Main){
dismiss()
}
@ -117,8 +126,7 @@ class DownloadsAlreadyExistDialog : BottomSheetDialogFragment(), AlreadyExistsAd
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteProcessing()
downloadViewModel.deleteAllWithID(duplicates.map { it.downloadItem.id })
downloadViewModel.deleteWithDuplicateStatus()
}
}

View file

@ -504,7 +504,10 @@ class FormatSelectionBottomSheetDialog(
finalFormats = if (items.first()?.type == Type.audio){
formatSorter.sortAudioFormats(finalFormats)
}else{
formatSorter.sortVideoFormats(finalFormats)
val audioFormats = finalFormats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
val videoFormats = finalFormats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }
formatSorter.sortVideoFormats(videoFormats) + formatSorter.sortAudioFormats(audioFormats)
}
}
FormatCategory.SMALLEST -> {

View file

@ -179,9 +179,9 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
ok.isEnabled = false
lifecycleScope.launch(Dispatchers.IO) {
val checkedItems = listAdapter.getCheckedItems()
val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) }
val checkedResultItems = items.filter { item -> checkedItems.contains(item.url) }
if (checkedResultItems.size == 1){
val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url)!!
val resultItem = resultViewModel.getItemByURL(checkedResultItems[0].url)!!
withContext(Dispatchers.Main){
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf(
Pair("result", resultItem),

View file

@ -1,23 +1,15 @@
package com.deniscerri.ytdl.ui.downloads
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.content.SharedPreferences
import android.os.Bundle
import android.os.IBinder
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.core.view.get
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -26,12 +18,10 @@ import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.inflate
import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.downloadcard.DownloadAudioFragment
import com.deniscerri.ytdl.util.Extensions.createBadge
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.NotificationUtil
@ -42,7 +32,6 @@ import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

View file

@ -164,10 +164,11 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyViewModel.getFilteredList().observe(viewLifecycleOwner) {
historyAdapter!!.submitList(it)
historyList = it
scrollToTop()
if (recyclerView.adapter == null){
recyclerView.adapter = historyAdapter
}else{
scrollToTop()
}
}

View file

@ -6,6 +6,7 @@ import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.interaction.DragInteraction
import androidx.core.content.edit
import androidx.preference.ListPreference
import androidx.preference.Preference
@ -13,12 +14,14 @@ import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.CleanUpLeftoverDownloads
import com.deniscerri.ytdl.work.DownloadWorker
import java.util.Calendar
import java.util.concurrent.TimeUnit
@ -69,6 +72,42 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
archivePathResultLauncher.launch(intent)
true
}
val workManager = WorkManager.getInstance(requireContext())
val cleanupLeftoverDownloads = findPreference<Preference>("cleanup_leftover_downloads")
cleanupLeftoverDownloads?.setOnPreferenceChangeListener { preference, newValue ->
var nextTime : Calendar? = Calendar.getInstance()
when(newValue) {
"daily" -> nextTime?.add(Calendar.DAY_OF_WEEK, 1)
"weekly" -> nextTime?.add(Calendar.DAY_OF_WEEK, 7)
"monthly" -> nextTime?.add(Calendar.MONTH, 1)
else -> nextTime = null
}
if (nextTime == null) workManager.cancelAllWorkByTag("cleanup_leftover_downloads")
else {
val workConstraints = Constraints.Builder()
val allowMeteredNetworks = preferences.getBoolean("metered_networks", true)
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
val delay = nextTime.timeInMillis.minus(System.currentTimeMillis())
val workRequest = OneTimeWorkRequestBuilder<CleanUpLeftoverDownloads>()
.addTag("cleanup_leftover_downloads")
.setConstraints(workConstraints.build())
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
workManager.enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
true
}
val scheduler = AlarmScheduler(requireContext())
val useAlarmManagerInsteadOfWorkManager = findPreference<SwitchPreferenceCompat>("use_alarm_for_scheduling")

View file

@ -11,6 +11,8 @@ import android.os.Environment
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
@ -19,10 +21,14 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.MoveCacheFilesWorker
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
@ -43,6 +49,7 @@ class FolderSettingsFragment : BaseSettingsFragment() {
private lateinit var preferences: SharedPreferences
private lateinit var editor: SharedPreferences.Editor
private lateinit var downloadViewModel: DownloadViewModel
private var activeDownloadCount = 0
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -50,6 +57,7 @@ class FolderSettingsFragment : BaseSettingsFragment() {
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
editor = preferences.edit()
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
musicPath = findPreference("music_path")
videoPath = findPreference("video_path")
@ -166,13 +174,18 @@ class FolderSettingsFragment : BaseSettingsFragment() {
clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}"
clearCache!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
if (activeDownloadCount == 0){
File(FileUtil.getCachePath(requireContext())).deleteRecursively()
Snackbar.make(requireView(), getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show()
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()
lifecycleScope.launch {
activeDownloadCount = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloadsCount()
}
if (activeDownloadCount == 0){
File(FileUtil.getCachePath(requireContext())).deleteRecursively()
Snackbar.make(requireView(), getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show()
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()
}
}
true
}

View file

@ -142,11 +142,10 @@ class FormatSorter(private var context: Context) {
val aIndex = videoResolutionOrder.indexOfFirst { a.format_note.contains(it, ignoreCase = true) }
val bIndex = videoResolutionOrder.indexOfFirst { b.format_note.contains(it, ignoreCase = true) }
if (aIndex > preferenceIndex || bIndex > preferenceIndex) {
-1
}else if(aIndex == -1 && bIndex == -1){
-1
0
}else{
bIndex.compareTo(aIndex)
}
@ -168,7 +167,6 @@ class FormatSorter(private var context: Context) {
return 0
}
}
return formats.sortedWith(fieldSorter)
}

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import android.content.res.Resources.NotFoundException
import android.os.Handler
import android.os.Looper
import android.text.Html
@ -26,6 +27,7 @@ import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLException
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
@ -112,23 +114,37 @@ class InfoUtil(private val context: Context) {
@Throws(JSONException::class)
fun getPlaylist(id: String, nextPageToken: String, playlistName: String): PlaylistTuple {
try{
try {
val items = arrayListOf<ResultItem>()
// -------------- PIPED API FUNCTION -------------------
var url = ""
url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id"
else """$pipedURL/nextpage/playlists/$id?nextpage=${nextPageToken.replace("&prettyPrint", "%26prettyPrint")}"""
else """$pipedURL/nextpage/playlists/$id?nextpage=${
nextPageToken.replace(
"&prettyPrint",
"%26prettyPrint"
)
}"""
println(url)
val res = genericRequest(url)
if (!res.has("relatedStreams")) throw Exception()
val dataArray = res.getJSONArray("relatedStreams")
var nextpage = res.getString("nextpage")
for (i in 0 until dataArray.length()){
val isMixPlaylist = nextPageToken.isBlank() && res.getInt("videos") < 0
if (isMixPlaylist) throw YoutubeDLException("This playlist type is unviewable.")
for (i in 0 until dataArray.length()) {
kotlin.runCatching {
val obj = dataArray.getJSONObject(i)
createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url"))?.apply {
playlistTitle = runCatching { res.getString("name") }.getOrElse { playlistName }
createVideoFromPipedJSON(
obj,
"https://youtube.com" + obj.getString("url")
)?.apply {
playlistTitle =
runCatching { res.getString("name") }.getOrElse { playlistName }
playlistURL = "https://www.youtube.com/playlist?list=$id"
items.add(this)
}
@ -137,11 +153,14 @@ class InfoUtil(private val context: Context) {
if (nextpage == "null") nextpage = ""
return PlaylistTuple(nextpage, items)
}catch (e: Exception){
Log.e("PLST", e.toString())
return PlaylistTuple(
"",
getFromYTDL("https://www.youtube.com/playlist?list=$id")
)
if (e is YoutubeDLException) {
throw(e)
}else{
return PlaylistTuple(
"",
getFromYTDL("https://www.youtube.com/playlist?list=$id")
)
}
}
}
@ -202,7 +221,7 @@ class InfoUtil(private val context: Context) {
Html.fromHtml(obj.getString("uploader").toString()).toString()
}catch (e: Exception){
Html.fromHtml(obj.getString("uploaderName").toString()).toString()
}.replace(" - Topic", "")
}.removeSuffix(" - Topic")
val duration = obj.getInt("duration").toStringDuration(Locale.US)
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
@ -1222,18 +1241,20 @@ class InfoUtil(private val context: Context) {
val ext = downloadItem.container
val preferredLanguage = sharedPreferences.getString("audio_language","")!!
println(audioQualityId)
if (audioQualityId.isNotBlank()) {
if(audioQualityId.contains("-")){
if (audioQualityId.matches(".*-[0-9]+.*".toRegex())) {
audioQualityId = if(!downloadItem.format.lang.isNullOrBlank() && downloadItem.format.lang != "None"){
"ba[format_id~='^(${audioQualityId.split("-")[0]})'][language^=${downloadItem.format.lang}]/ba/b"
}else{
"$audioQualityId/${audioQualityId.split("-")[0]}"
}
}
request.addOption("-f", audioQualityId)
}else{
//enters here if generic or quick downloaded with ba format
val preferredLanguage = sharedPreferences.getString("audio_language","")!!
if (preferredLanguage.isNotBlank()){
request.addOption("-f", "ba[language^=$preferredLanguage]/ba/b")
}
@ -1257,6 +1278,10 @@ class InfoUtil(private val context: Context) {
}
}
if (preferredLanguage.isNotBlank()) {
formatSorting.append(",lang:${preferredLanguage}")
}
request.addOption("-P", downDir.absolutePath)
request.addOption("-S", formatSorting.toString())
@ -1357,7 +1382,8 @@ class InfoUtil(private val context: Context) {
var audioF = downloadItem.videoPreferences.audioFormatIDs.map { f ->
val format = downloadItem.allFormats.find { it.format_id == f }
format?.run {
if (this.format_id.contains("-")) {
println(format_id)
if (this.format_id.matches(".*-[0-9]+".toRegex())) {
if (!this.lang.isNullOrBlank() && this.lang != "None") {
"ba[format_id~='^(${this.format_id.split("-")[0]})'][language^=${this.lang}]"
} else {
@ -1477,13 +1503,11 @@ class InfoUtil(private val context: Context) {
}
val genericFormats = getGenericVideoFormats(context.resources).map { it.format_id }
val preferredLanguage = sharedPreferences.getString("audio_language","")!!
StringBuilder().apply {
if (hasGenericResulutionFormat.isNotBlank()) {
append(",res:${hasGenericResulutionFormat}")
}else if (genericFormats.contains(videoF) && preferredQuality!!.contains("p_")){
append(",res:${preferredQuality.split("_")[0]}")
}
if (sharedPreferences.getBoolean("prefer_smaller_formats", false)) append(",+size")
if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref")
@ -1491,6 +1515,7 @@ class InfoUtil(private val context: Context) {
if (cont.isNotBlank()) append(",vext:$cont")
if (acont.isNotBlank()) append(",aext:$acont")
if (abrSort.isNotBlank()) append(",abr~${abrSort}")
if (preferredLanguage.isNotBlank()) append(",lang:${preferredLanguage}")
if (this.isNotBlank()){
request.addOption("-S", "+hasaud$this")
}

View file

@ -474,6 +474,27 @@ class NotificationUtil(var context: Context) {
notificationManager.cancel(DOWNLOAD_ERRORED_NOTIFICATION_ID + id)
}
fun createDeletingLeftoverDownloadsNotification() : Notification {
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
return notificationBuilder
.setContentTitle(resources.getString(R.string.cleanup_leftover_downloads))
.setCategory(Notification.CATEGORY_EVENT)
.setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon(
BitmapFactory.decodeResource(
resources,
R.drawable.ic_launcher_foreground_large
)
)
.setContentText("")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.clearActions()
.build()
}
fun createMoveCacheFilesNotification(pendingIntent: PendingIntent?, downloadMiscChannelId: String): Notification {
val notificationBuilder = getBuilder(downloadMiscChannelId)

View file

@ -0,0 +1,42 @@
package com.deniscerri.ytdl.work
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.google.android.material.snackbar.Snackbar
import java.io.File
class CleanUpLeftoverDownloads(
private val context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val notificationUtil = NotificationUtil(App.instance)
val id = System.currentTimeMillis().toInt()
val notification = notificationUtil.createDeletingLeftoverDownloadsNotification()
val foregroundInfo = ForegroundInfo(id, notification)
setForegroundAsync(foregroundInfo)
val dbManager = DBManager.getInstance(context)
val downloadRepo = DownloadRepository(dbManager.downloadDao)
downloadRepo.deleteCancelled()
downloadRepo.deleteErrored()
val activeDownloadCount = downloadRepo.getActiveDownloadsCount()
if (activeDownloadCount == 0){
File(FileUtil.getCachePath(context)).deleteRecursively()
}
return Result.success()
}
}

View file

@ -159,7 +159,7 @@
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
android:paddingBottom="200dp"
app:spanCount="2"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/>

View file

@ -149,7 +149,7 @@
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
android:paddingBottom="200dp"
app:spanCount="3"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/>

View file

@ -157,7 +157,7 @@
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
android:paddingBottom="200dp"
app:spanCount="2"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/>

View file

@ -149,7 +149,7 @@
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
android:paddingBottom="200dp"
app:spanCount="4"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/>

View file

@ -105,25 +105,25 @@
<TextView
android:id="@+id/audio_formats"
style="@style/Widget.Material3.FloatingActionButton.Large.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorPrimaryContainer"
android:backgroundTint="?attr/colorPrimary"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textColor="@color/white"
android:textCursorDrawable="@string/audio_format"
android:textStyle="bold"
app:cornerRadius="10dp"
app:drawableStartCompat="@drawable/ic_music_formatcard"
app:drawableTint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
@ -180,8 +180,7 @@
android:ellipsize="end"
android:gravity="bottom|end"
android:maxWidth="70dp"
android:maxLines="2"
app:layout_constraintBottom_toBottomOf="@+id/format_note"
android:maxLines="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View file

@ -92,8 +92,7 @@
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
android:paddingBottom="200dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/>

View file

@ -31,6 +31,7 @@
<TextView
android:id="@+id/drag"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="10dp"

View file

@ -12,6 +12,12 @@
app:launchSingleTop="true"
app:popUpTo="@id/downloadBottomSheetDialog"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_downloadBottomSheetDialog_to_downloadsAlreadyExistDialog2"
app:destination="@id/downloadsAlreadyExistDialog2"
app:launchSingleTop="true"
app:popUpTo="@id/downloadBottomSheetDialog"
app:popUpToInclusive="true" />
</dialog>
<dialog
android:id="@+id/selectPlaylistItemsDialog"
@ -33,7 +39,14 @@
<dialog
android:id="@+id/downloadMultipleBottomSheetDialog"
android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadMultipleBottomSheetDialog"
android:label="DownloadMultipleBottomSheetDialog" />
android:label="DownloadMultipleBottomSheetDialog" >
<action
android:id="@+id/action_downloadMultipleBottomSheetDialog_to_downloadsAlreadyExistDialog2"
app:destination="@id/downloadsAlreadyExistDialog2"
app:launchSingleTop="true"
app:popUpTo="@id/downloadMultipleBottomSheetDialog"
app:popUpToInclusive="true" />
</dialog>
<dialog
android:id="@+id/downloadsAlreadyExistDialog2"
android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadsAlreadyExistDialog"

View file

@ -411,4 +411,7 @@
<string name="recode_video">Rikodo Videon</string>
<string name="recode_video_summary">Rikodon skedarin e videos sipas formatit të cilësuar nga përdoruesi</string>
<string name="restore_info">Shtyp \'Rikthe\' për të kombinuar të dhënat e ruajtura me të dhënat aktuale. Shtyp \'Rivendos\' për të fshirë gjithçka dhe vetëm përdor të dhënat e ruajtura në skedar.</string>
<string name="daily">Çdo ditë</string>
<string name="weekly">Çdo javë</string>
<string name="monthly">Çdo muaj</string>
</resources>

View file

@ -1475,4 +1475,18 @@
<item>never</item>
</string-array>
<string-array name="cleanup_leftover_downloads">
<item>@string/disabled</item>
<item>@string/daily</item>
<item>@string/weekly</item>
<item>@string/monthly</item>
</string-array>
<string-array name="cleanup_leftover_downloads_values">
<item></item>
<item>daily</item>
<item>weekly</item>
<item>monthly</item>
</string-array>
</resources>

View file

@ -414,4 +414,8 @@
<string name="use_item_url_not_playlist_summary">Useful for Libretube offline playlists that are not recognised by yt-dlp. Playlist metadata wont be embedded with this enabled</string>
<string name="recode_video">Recode Video</string>
<string name="recode_video_summary">Recodes the video file to the specified video format</string>
<string name="daily">Daily</string>
<string name="weekly">Weekly</string>
<string name="monthly">Monthly</string>
<string name="cleanup_leftover_downloads">Clean-up leftover downloads (cancelled, errored)</string>
</resources>

View file

@ -145,6 +145,15 @@
app:summary="@string/log_downloads_summary"
app:title="@string/log_downloads" />
<ListPreference
android:defaultValue=""
android:entries="@array/cleanup_leftover_downloads"
android:entryValues="@array/cleanup_leftover_downloads_values"
android:icon="@drawable/baseline_delete_24"
app:key="cleanup_leftover_downloads"
app:useSimpleSummaryProvider="true"
app:title="@string/cleanup_leftover_downloads" />
<EditTextPreference
app:key="retries"
android:icon="@drawable/ic_retries"

View file

@ -63,4 +63,25 @@ Add --recode-video toggle in the settings to use instead of --merge-output-forma
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 not selecting an audio format to show in the format view in the download card after updating formats
download_rescheduled_to shows up twice, one in original language and one in english
It's very difficult to tap on video icon for the video at the bottom
#529
https://www.instagram.com/reel/C9zTRlONU6G/?igsh=MWJ1djBhYjdscG5wMw== chooses smallest format
Limit audio id length
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)