This commit is contained in:
zaednasr 2024-06-11 22:57:59 +02:00
parent f724b14dfa
commit 3c5167f6eb
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
15 changed files with 193 additions and 477 deletions

View file

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

View file

@ -18,10 +18,6 @@
<uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- for display over apps-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- alarm scheduler-->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
@ -434,8 +430,6 @@
</intent-filter>
</activity>
<service android:name=".services.ProcessDownloadsInBackgroundService" android:exported="false" />
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"

View file

@ -38,9 +38,9 @@ interface DownloadDao {
@Query("""
SELECT DISTINCT type from downloads where status = 'Processing' and id in (:ids)
SELECT DISTINCT type from downloads where status = 'Processing'
""")
fun getProcessingDownloadTypes(ids: List<Long>) : List<String>
fun getProcessingDownloadTypes() : List<String>
@Query("UPDATE downloads set status = 'Processing' WHERE id in (:ids)")
@ -56,6 +56,12 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloadsList() : List<DownloadItem>
@Query("UPDATE downloads set downloadStartTime=:time, status='Scheduled' WHERE status ='Processing'")
suspend fun updateProcessingDownloadTime(time: Long)
@Query("UPDATE downloads set downloadPath=:path WHERE status ='Processing'")
suspend fun updateProcessingDownloadPath(path: String)
@Query("SELECT * FROM downloads WHERE status='Active'")
suspend fun getActiveDownloadsList() : List<DownloadItem>
@ -126,12 +132,6 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE id IN (:ids)")
fun getDownloadsByIds(ids: List<Long>) : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Processing' AND id >=:id")
fun getProcessingItemsFromID(id: Long) : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Processing' AND id >=:id and id<=:id2")
fun getProcessingItemsBetweenIDs(id: Long, id2: Long) : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE id IN (:ids)")
fun getDownloadsByIdsFlow(ids: List<Long>) : Flow<List<DownloadItem>>
@ -231,8 +231,8 @@ interface DownloadDao {
@Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)")
suspend fun reQueueDownloadItems(list: List<Long>)
@Query("Update downloads SET status='Saved' WHERE status='Processing' and id in (:ids)")
fun updateProcessingtoSavedStatus(ids: List<Long>)
@Query("Update downloads SET status='Saved' WHERE status='Processing'")
suspend fun updateProcessingtoSavedStatus()
@Transaction
suspend fun putAtTopOfTheQueue(existingIDs: List<Long>){

View file

@ -3,8 +3,11 @@ package com.deniscerri.ytdl.database.repository
import android.annotation.SuppressLint
import android.content.Context
import android.net.ConnectivityManager
import android.os.Handler
import android.os.Looper
import android.text.format.DateFormat
import android.widget.Toast
import androidx.core.os.postDelayed
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.preference.PreferenceManager
@ -28,6 +31,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
@ -109,14 +114,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getDownloadById(id)
}
fun getProcessingItemsFromID(id: Long) : Flow<List<DownloadItem>> {
return downloadDao.getProcessingItemsFromID(id)
}
fun getProcessingItemsBetweenIDs(first: Long, last:Long) : List<DownloadItem> {
return downloadDao.getProcessingItemsBetweenIDs(first, last)
}
fun getAllItemsByIDs(ids : List<Long>) : List<DownloadItem>{
return downloadDao.getDownloadsByIds(ids)
}
@ -137,6 +134,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getActiveAndQueuedDownloadsList()
}
suspend fun updateProcessingDownloadTime(time: Long) {
downloadDao.updateProcessingDownloadTime(time)
}
fun getActiveAndQueuedDownloadIDs() : List<Long> {
return downloadDao.getActiveAndQueuedDownloadIDs()
}
@ -258,12 +259,23 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
}
val handler = Handler(Looper.getMainLooper())
val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered
if (!allowMeteredNetworks && isCurrentNetworkMetered){
Looper.prepare().run {
handler.postDelayed({
Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show()
}
}, 0)
}
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)
}
}
}

View file

@ -9,11 +9,8 @@ import android.util.DisplayMetrics
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.media3.exoplayer.offline.Download
import androidx.paging.PagingData
import androidx.preference.PreferenceManager
import androidx.work.Data
@ -46,21 +43,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Locale
@ -75,8 +60,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val allDownloads : Flow<PagingData<DownloadItem>>
val queuedDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloads : Flow<List<DownloadItem>>
val allProcessingDownloads : Flow<List<DownloadItem>>
var mostRecentProcessingDownloads: Flow<List<DownloadItem>>
val processingDownloads : Flow<List<DownloadItem>>
val cancelledDownloads : Flow<PagingData<DownloadItemSimple>>
val erroredDownloads : Flow<PagingData<DownloadItemSimple>>
val savedDownloads : Flow<PagingData<DownloadItemSimple>>
@ -115,8 +99,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
auto, audio, video, command
}
val processingDownloads = MediatorLiveData<List<DownloadItem>>(listOf())
var processingItemsFlow : ProcessingItemsJob? = null
var processingItems = MutableStateFlow(false)
@ -143,8 +125,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
allDownloads = repository.allDownloads.flow
queuedDownloads = repository.queuedDownloads.flow
activeDownloads = repository.activeDownloads
allProcessingDownloads = repository.processingDownloads
mostRecentProcessingDownloads = repository.getProcessingItemsFromID(0)
processingDownloads = repository.processingDownloads
savedDownloads = repository.savedDownloads.flow
scheduledDownloads = repository.scheduledDownloads.flow
cancelledDownloads = repository.cancelledDownloads.flow
@ -385,6 +366,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
fun turnDownloadItemsToProcessingDownloads(itemIDs: List<Long>) = viewModelScope.launch(Dispatchers.IO){
updateProcessingJobData(ProcessingItemsJob(null, DownloadItem::class.java.toString(), itemIDs))
val job = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessing()
processingItems.emit(true)
try {
itemIDs.forEachIndexed { idx, it ->
@ -395,7 +377,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
item.status = DownloadRepository.Status.Processing.toString()
processingItemsFlow?.apply {
val id = repository.insert(item)
if (idx == 0) mostRecentProcessingDownloads = repository.getProcessingItemsFromID(id)
processingDownloadItemIDs.add(id)
updateProcessingJobData(this)
}
@ -499,7 +480,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.deleteProcessing()
}
fun deleteAllWithID(ids: List<Long>) = viewModelScope.launch(Dispatchers.IO){
suspend fun deleteAllWithID(ids: List<Long>) {
repository.deleteAllWithIDs(ids)
}
@ -618,6 +599,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.startDownloadWorker(emptyList(), application)
}
suspend fun queueProcessingDownloads(){
val processingItems = repository.getProcessingDownloads()
queueDownloads(processingItems)
}
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : List<SharedDownloadViewModel.AlreadyExistsIDs> {
val ids = sharedDownloadViewModel.queueDownloads(items, ign)
return ids
@ -639,43 +625,33 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dbManager.downloadDao.getDownloadIDsNotPresentInList(items.ifEmpty { listOf(-1L) }, status.map { it.toString() })
}
fun moveProcessingToSavedCategory(ids: List<Long>){
ids.chunked(100).forEach {
dao.updateProcessingtoSavedStatus(it)
suspend fun moveProcessingToSavedCategory(){
dao.updateProcessingtoSavedStatus()
}
suspend fun updateProcessingFormat(selectedFormats: List<FormatTuple>): List<Long> {
val items = repository.getProcessingDownloads()
items.forEachIndexed { index, i ->
selectedFormats[index].format?.apply {
i.format = this
}
if (i.type == Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
repository.update(i)
}
return items.map { itm -> itm.format.filesize }
}
suspend fun updateProcessingCommandFormat(format: Format){
val items = repository.getProcessingDownloads()
items.forEach {
it.format = format
repository.update(it)
}
}
suspend fun updateProcessingFormat(ids: List<Long>, selectedFormats: List<FormatTuple>): List<Long> {
return ids.chunked(100).map {
val items = repository.getAllItemsByIDs(it)
items.forEachIndexed { index, i ->
selectedFormats[index].format?.apply {
i.format = this
}
if (i.type == Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
repository.update(i)
}
items.map { itm -> itm.format.filesize }
}.flatten()
}
suspend fun updateProcessingCommandFormat(ids: List<Long>, format: Format){
ids.chunked(100).forEach {
repository.getAllItemsByIDs(it).forEach { i ->
i.format = format
repository.update(i)
}
}
}
suspend fun updateProcessingDownloadPath(ids: List<Long>, path: String){
ids.chunked(100).forEach {
repository.getAllItemsByIDs(it).forEach { i ->
i.downloadPath = path
repository.update(i)
}
}
suspend fun updateProcessingDownloadPath(path: String){
dao.updateProcessingDownloadPath(path)
}
fun getProcessingDownloadsCount() : Int {
@ -687,42 +663,35 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
suspend fun updateProcessingAllFormats(ids: List<Long>, formatCollection: List<List<Format>>) {
ids.chunked(100).forEach {
val items = repository.getAllItemsByIDs(it)
items.forEachIndexed { index, i ->
i.allFormats.clear()
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) {
runCatching {
i.allFormats.addAll(formatCollection[index])
}
suspend fun updateProcessingAllFormats(formatCollection: List<List<Format>>) {
val items = repository.getProcessingDownloads()
items.forEachIndexed { index, i ->
i.allFormats.clear()
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) {
runCatching {
i.allFormats.addAll(formatCollection[index])
}
i.format = getFormat(i.allFormats, i.type)
kotlin.runCatching {
dbManager.resultDao.getResultByURL(i.url)?.apply {
this.formats = formatCollection[index].toMutableList()
dbManager.resultDao.update(this)
}
}
repository.update(i)
}
i.format = getFormat(i.allFormats, i.type)
kotlin.runCatching {
dbManager.resultDao.getResultByURL(i.url)?.apply {
this.formats = formatCollection[index].toMutableList()
dbManager.resultDao.update(this)
}
}
repository.update(i)
}
}
suspend fun continueUpdatingFormatsOnBackground(itemIDs: List<Long>){
itemIDs.chunked(100).forEach {list ->
repository.getAllItemsByIDs(list).onEach {
it.status = DownloadRepository.Status.Saved.toString()
repository.update(it)
}
}
suspend fun continueUpdatingFormatsOnBackground(){
val ids = dao.getProcessingDownloadsList().map { it.id }
dao.updateProcessingtoSavedStatus()
val id = System.currentTimeMillis().toInt()
val workRequest = OneTimeWorkRequestBuilder<UpdatePlaylistFormatsWorker>()
.setInputData(
Data.Builder()
.putLongArray("ids", itemIDs.toLongArray())
.putLongArray("ids", ids.toLongArray())
.putInt("id", id)
.build())
.addTag("updateFormats")
@ -736,23 +705,22 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
suspend fun updateProcessingType(ids: List<Long>, newType: Type) {
ids.chunked(100).forEach { _ ->
repository.getAllItemsByIDs(ids).apply {
val new = switchDownloadType(this, newType)
new.forEach {
repository.update(it)
}
suspend fun updateProcessingType(newType: Type) {
val processing = repository.getProcessingDownloads()
processing.apply {
val new = switchDownloadType(this, newType)
new.forEach {
repository.update(it)
}
}
}
fun checkIfAllProcessingItemsHaveSameType(ids: List<Long>) : Pair<Boolean, Type> {
val types = mutableSetOf<String>()
ids.chunked(100).forEach {
types.addAll(dao.getProcessingDownloadTypes(it))
}
suspend fun updateProcessingDownloadTime(time: Long) {
repository.updateProcessingDownloadTime(time)
}
fun checkIfAllProcessingItemsHaveSameType() : Pair<Boolean, Type> {
val types = dao.getProcessingDownloadTypes()
return Pair(types.size == 1, Type.valueOf(types.first()))
}

View file

@ -321,7 +321,7 @@ class SharedDownloadViewModel(private val context: Context) {
val prefVideo = sharedPreferences.getString("format_importance_video", itemValues.joinToString(","))!!
prefVideo.split(",").forEachIndexed { idx, s ->
val importance = (itemValues.size - idx) * 10
var importance = (itemValues.size - idx) * 10
when(s) {
"id" -> {
@ -345,8 +345,9 @@ class SharedDownloadViewModel(private val context: Context) {
for(i in 0..preferenceIndex){
removeAt(0)
}
add(0, preference)
requirements.add { it: Format -> if (it.format_note.contains(preference, ignoreCase = true)) importance else 0 }
forEachIndexed { index, res ->
if (index >= 2) importance = 0
requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (importance - index - 1) else 0 }
}
}

View file

@ -1,165 +0,0 @@
package com.deniscerri.ytdl.services
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.content.IntentCompat
import androidx.lifecycle.lifecycleScope
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.SharedDownloadViewModel
import com.deniscerri.ytdl.util.NotificationUtil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ProcessDownloadsInBackgroundService : Service() {
private val binder: IBinder = LocalBinder()
private val queueProcessingDownloadsJobList = mutableListOf<Job>()
private lateinit var repository: DownloadRepository
private lateinit var resultRepository: ResultRepository
private lateinit var downloadViewModel: SharedDownloadViewModel
inner class LocalBinder: Binder() {
val service: ProcessDownloadsInBackgroundService
get() = this@ProcessDownloadsInBackgroundService
}
override fun onCreate() {
super.onCreate()
val dbManager = DBManager.getInstance(this)
repository = DownloadRepository(dbManager.downloadDao)
resultRepository = ResultRepository(dbManager.resultDao, this)
downloadViewModel = SharedDownloadViewModel(this)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val binding = intent.getBooleanExtra("binding", false)
if (binding) {
val cancel = intent.getBooleanExtra("cancel", false)
if (cancel) {
cancelAllProcessingJobs()
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO){
repository.deleteProcessing()
withContext(Dispatchers.Main){
if (Build.VERSION.SDK_INT > 23){
stopForeground(STOP_FOREGROUND_REMOVE)
}else{
stopForeground(true)
}
stopSelf()
}
}
}else{
return super.onStartCommand(intent, flags, startId)
}
}
val notificationUtil = NotificationUtil(this)
startForeground(System.currentTimeMillis().toInt(), notificationUtil.createProcessingDownloads())
val itemType = intent.getStringExtra("itemType")!!
val itemIDs = intent.getLongArrayExtra("itemIDs")!!.toList()
val processingItemIDs = intent.getLongArrayExtra("processingItemIDs")!!.toList()
val timeInMillis = intent.getLongExtra("timeInMillis", 0)
val processingFinished = intent.getBooleanExtra("processingFinished", true)
val job = CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
if (!processingFinished) {
when(itemType) {
ResultItem::class.java.toString() -> {
itemIDs.chunked(100).map { ids ->
resultRepository.getAllByIDs(ids).map {
downloadViewModel.createDownloadItemFromResult(
result = it, givenType = DownloadViewModel.Type.valueOf(
downloadViewModel.getDownloadType(url = it.url).toString()
)
)
}.apply {
if (timeInMillis > 0) {
this.forEach {
it.setAsScheduling(timeInMillis)
}
}
if (isActive){
downloadViewModel.queueDownloads(this)
}
}
}
}
DownloadItem::class.java.toString() -> {
itemIDs.chunked(100).map { ids ->
repository.getAllItemsByIDs(ids).apply {
if (timeInMillis > 0) {
this.forEach {
it.setAsScheduling(timeInMillis)
}
}
if (isActive){
downloadViewModel.queueDownloads(this)
}
}
}
}
}
}else {
repository.getProcessingItemsBetweenIDs(processingItemIDs.first(), processingItemIDs.last()).apply {
this.chunked(100).map {
if (timeInMillis > 0){
this.forEach { d ->
d.setAsScheduling(timeInMillis)
}
}
if (isActive){
downloadViewModel.queueDownloads(it)
}
}
}
}
}
job.invokeOnCompletion {
queueProcessingDownloadsJobList.remove(job)
if (queueProcessingDownloadsJobList.isEmpty()){
stopForeground(true)
stopSelf()
}
}
queueProcessingDownloadsJobList.add(job)
return super.onStartCommand(intent, flags, startId)
}
override fun onBind(intent: Intent): IBinder {
return binder
}
private fun DownloadItem.setAsScheduling(timeInMillis: Long) {
status = DownloadRepository.Status.Scheduled.toString()
downloadStartTime = timeInMillis
}
fun cancelAllProcessingJobs(){
queueProcessingDownloadsJobList.onEach { it.cancel(CancellationException()) }
}
}

View file

@ -3,32 +3,23 @@ package com.deniscerri.ytdl.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
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.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.os.IBinder
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.core.view.forEach
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
@ -45,13 +36,11 @@ 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.services.ProcessDownloadsInBackgroundService
import com.deniscerri.ytdl.ui.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.Shimmer
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -61,22 +50,14 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.function.Predicate
class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), ConfigureMultipleDownloadsAdapter.OnItemClickListener, View.OnClickListener,
ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
@ -91,6 +72,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var bottomAppBar: BottomAppBar
private lateinit var filesize : TextView
private lateinit var count : TextView
private lateinit var downloadBtn : MaterialButton
private lateinit var scheduleBtn : MaterialButton
private lateinit var title: TextView
private lateinit var subtitle: TextView
private lateinit var shimmerTitle: ShimmerFrameLayout
@ -154,19 +137,14 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
itemTouchHelper.attachToRecyclerView(recyclerView)
}
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
val download = view.findViewById<Button>(R.id.bottomsheet_download_button)
scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
downloadBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_download_button)
bottomAppBar = view.findViewById(R.id.bottomAppBar)
val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type)
filesize = view.findViewById(R.id.filesize)
count = view.findViewById(R.id.count)
val continueInBackgroundSnackBar = Snackbar.make(recyclerView, R.string.process_downloads_background, Snackbar.LENGTH_LONG)
val snackbarView: View = continueInBackgroundSnackBar.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
title = view.findViewById(R.id.bottom_sheet_title)
subtitle = view.findViewById(R.id.bottom_sheet_subtitle)
shimmerTitle = view.findViewById(R.id.shimmer_loading_title)
@ -174,17 +152,12 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch {
downloadViewModel.processingItems.collectLatest {
val loading = it
toggleLoadingShimmerTitle(loading)
bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading }
if (!loading){
continueInBackgroundSnackBar.dismiss()
}
toggleLoading(it)
}
}
lifecycleScope.launch {
downloadViewModel.mostRecentProcessingDownloads.collectLatest { items ->
downloadViewModel.processingDownloads.collectLatest { items ->
processingItemsCount = items.size
count.text = "${processingItemsCount} ${getString(R.string.selected)}"
listAdapter.submitList(items)
@ -224,71 +197,31 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
scheduleBtn.setOnClickListener{
fun initSchedule(processingFinished: Boolean) {
UiUtil.showDatePicker(parentFragmentManager) { cal ->
scheduleBtn.isEnabled = false
download.isEnabled = false
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
}
getProcessingItemsData()?.apply {
this.job?.cancel(CancellationException())
this.job = null
downloadProcessDownloadsInBackground(this, processingFinished)
}
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(cal.timeInMillis)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
dismiss()
}
}
}
if (shimmerTitle.isVisible){
continueInBackgroundSnackBar.setAction(R.string.ok) {
initSchedule(false)
}
continueInBackgroundSnackBar.show()
}else{
initSchedule(true)
}
}
download!!.setOnClickListener {
fun initDownload(processingFinished: Boolean) {
scheduleBtn.isEnabled = false
download.isEnabled = false
UiUtil.showDatePicker(parentFragmentManager) { cal ->
toggleLoading(true)
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.updateProcessingDownloadTime(cal.timeInMillis)
downloadViewModel.deleteAllWithID(currentDownloadIDs)
}
getProcessingItemsData()?.apply {
this.job?.cancel(CancellationException())
this.job = null
downloadProcessDownloadsInBackground(this, processingFinished)
downloadViewModel.queueProcessingDownloads()
}
dismiss()
}
}
if (shimmerTitle.isVisible){
continueInBackgroundSnackBar.setAction(R.string.ok) {
initDownload(false)
}
continueInBackgroundSnackBar.show()
}else{
initDownload(true)
}
}
download.setOnLongClickListener {
downloadBtn.setOnClickListener {
toggleLoading(true)
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.queueProcessingDownloads()
}
dismiss()
}
}
downloadBtn.setOnLongClickListener {
val dd = MaterialAlertDialogBuilder(requireContext())
dd.setTitle(getString(R.string.save_for_later))
dd.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
@ -296,7 +229,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch{
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.moveProcessingToSavedCategory(getProcessingDownloadItemIDs())
downloadViewModel.moveProcessingToSavedCategory()
}
getProcessingItemsData()?.apply {
this.job?.cancel(CancellationException())
@ -312,13 +245,13 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val formatListener = object : OnFormatClickListener {
override fun onFormatClick(selectedFormats: List<FormatTuple>) {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingFormat(getProcessingDownloadItemIDs(), selectedFormats)
downloadViewModel.updateProcessingFormat(selectedFormats)
}
}
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingAllFormats(getProcessingDownloadItemIDs(), allFormats)
downloadViewModel.updateProcessingAllFormats(allFormats)
}
}
@ -326,7 +259,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
override fun onContinueOnBackground() {
requireActivity().lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.continueUpdatingFormatsOnBackground(getProcessingDownloadItemIDs())
downloadViewModel.continueUpdatingFormatsOnBackground()
}
getProcessingItemsData()?.apply {
this.job?.cancel(CancellationException())
@ -369,7 +302,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
audio!!.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingType(getProcessingDownloadItemIDs(), DownloadViewModel.Type.audio)
downloadViewModel.updateProcessingType(DownloadViewModel.Type.audio)
withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
@ -381,7 +314,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
video!!.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch{
downloadViewModel.updateProcessingType(getProcessingDownloadItemIDs(), DownloadViewModel.Type.video)
downloadViewModel.updateProcessingType(DownloadViewModel.Type.video)
withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
@ -393,7 +326,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
command!!.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch{
downloadViewModel.updateProcessingType(getProcessingDownloadItemIDs(), DownloadViewModel.Type.command)
downloadViewModel.updateProcessingType(DownloadViewModel.Type.command)
withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
@ -417,7 +350,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
R.id.format -> {
lifecycleScope.launch {
val res = withContext(Dispatchers.IO){
downloadViewModel.checkIfAllProcessingItemsHaveSameType(getProcessingDownloadItemIDs())
downloadViewModel.checkIfAllProcessingItemsHaveSameType()
}
if (!res.first){
Toast.makeText(requireContext(), getString(R.string.format_filtering_hint), Toast.LENGTH_SHORT).show()
@ -435,13 +368,15 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
it.joinToString(" ") { c -> c.content }
)
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingCommandFormat(getProcessingDownloadItemIDs(), format)
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.updateProcessingCommandFormat(format)
}
}
}
}else{
val items = withContext(Dispatchers.IO){
downloadViewModel.getAllByIDs(getProcessingDownloadItemIDs())
downloadViewModel.getProcessingDownloads()
}
val flatFormatCollection = items.map { it.allFormats }.flatten()
val commonFormats = withContext(Dispatchers.IO){
@ -465,7 +400,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
R.id.more -> {
lifecycleScope.launch {
val res = withContext(Dispatchers.IO){
downloadViewModel.checkIfAllProcessingItemsHaveSameType(getProcessingDownloadItemIDs())
downloadViewModel.checkIfAllProcessingItemsHaveSameType()
}
if (!res.first) {
Toast.makeText(
@ -486,7 +421,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding)
val items = withContext(Dispatchers.IO){
downloadViewModel.getAllByIDs(getProcessingDownloadItemIDs())
downloadViewModel.getProcessingDownloads()
}
UiUtil.configureAudio(
@ -567,7 +502,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding)
val items = withContext(Dispatchers.IO){
downloadViewModel.getAllByIDs(getProcessingDownloadItemIDs())
downloadViewModel.getProcessingDownloads()
}
UiUtil.configureVideo(
@ -664,19 +599,27 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
private fun toggleLoadingShimmerTitle(show: Boolean) {
shimmerTitle.isVisible = show
title.isVisible = !show
shimmerSubtitle.isVisible = show
subtitle.isVisible = !show
private fun toggleLoading(loading: Boolean){
shimmerTitle.isVisible = loading
title.isVisible = !loading
shimmerSubtitle.isVisible = loading
subtitle.isVisible = !loading
if (show){
if (loading){
shimmerTitle.startShimmer()
shimmerSubtitle.startShimmer()
}else{
shimmerTitle.stopShimmer()
shimmerSubtitle.stopShimmer()
}
scheduleBtn.isEnabled = !loading
downloadBtn.isEnabled = !loading
bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading }
}
private fun toggleLoadingShimmerTitle(show: Boolean) {
}
private fun updateFileSize(items: List<Long>){
@ -705,7 +648,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingDownloadPath(getProcessingDownloadItemIDs(), result.data?.data.toString())
downloadViewModel.updateProcessingDownloadPath(result.data?.data.toString())
}
val path = FileUtil.formatPath(result.data!!.data.toString())
@ -837,12 +780,18 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
override fun onDismiss(dialog: DialogInterface) {
getProcessingItemsData()?.apply {
if (this.job != null){
this.job?.cancel(CancellationException())
downloadViewModel.deleteAllWithID(this.processingDownloadItemIDs)
lifecycleScope.launch {
withContext(Dispatchers.IO){
getProcessingItemsData()?.apply {
if (this.job?.isActive == true){
this.job?.cancel(CancellationException())
downloadViewModel.deleteAllWithID(this.processingDownloadItemIDs)
}
}
downloadViewModel.deleteProcessing()
}
}
super.onDismiss(dialog)
}
@ -923,24 +872,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
}
private fun getProcessingDownloadItemIDs() : List<Long> {
return downloadViewModel.processingItemsFlow?.processingDownloadItemIDs ?: listOf()
}
private fun getProcessingItemsData() : DownloadViewModel.ProcessingItemsJob? {
return downloadViewModel.processingItemsFlow
}
private fun downloadProcessDownloadsInBackground(jobData: DownloadViewModel.ProcessingItemsJob, processingFinished: Boolean, timeInMillis: Long = 0){
Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent ->
intent.putExtra("itemType", jobData.originItemType)
intent.putExtra("itemIDs", jobData.originItemIDs.toLongArray())
intent.putExtra("processingItemIDs", jobData.processingDownloadItemIDs.toLongArray())
intent.putExtra("timeInMillis", timeInMillis)
intent.putExtra("processingFinished", processingFinished)
requireActivity().startService(intent)
}
}
}

View file

@ -257,7 +257,24 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
private fun checkRanges(start: String, end: String) : Boolean {
return start.isNotBlank() && end.isNotBlank()
fromTextInput.error = ""
toTextInput.error = ""
if (start.isBlank() || end.isBlank()) return false
val startValid = start.toInt() >= 0
val endValid = end.toInt() <= resultItemIDs.size
if (!startValid) {
fromTextInput.editText?.setText("")
fromTextInput.error = "Invalid Number"
}
if (!endValid) {
toTextInput.editText?.setText("")
toTextInput.error = "Invalid Number"
}
return startValid && endValid
}
private fun reset(){

View file

@ -29,7 +29,6 @@ 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.services.ProcessDownloadsInBackgroundService
import com.deniscerri.ytdl.util.Extensions.createBadge
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil
@ -266,7 +265,6 @@ class DownloadQueueMainFragment : Fragment(){
}
private fun cancelAllDownloads() {
cancelBackgroundProcessingDownloads()
workManager.cancelAllWorkByTag("download")
lifecycleScope.launch {
val notificationUtil = NotificationUtil(requireContext())
@ -280,26 +278,4 @@ class DownloadQueueMainFragment : Fragment(){
downloadViewModel.cancelActiveQueued()
}
}
private fun cancelBackgroundProcessingDownloads(){
val connection = object : ServiceConnection {
private lateinit var mService: ProcessDownloadsInBackgroundService
private var mBound: Boolean = false
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as ProcessDownloadsInBackgroundService.LocalBinder
mService = binder.service
mBound = true
}
override fun onServiceDisconnected(arg0: ComponentName) {
mBound = false
}
}
Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent ->
intent.putExtra("binding", true)
intent.putExtra("cancel", true)
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
}

View file

@ -26,7 +26,6 @@ import com.deniscerri.ytdl.receiver.CancelDownloadNotificationReceiver
import com.deniscerri.ytdl.receiver.CancelWorkReceiver
import com.deniscerri.ytdl.receiver.PauseDownloadNotificationReceiver
import com.deniscerri.ytdl.receiver.ResumeActivity
import com.deniscerri.ytdl.services.ProcessDownloadsInBackgroundService
import com.deniscerri.ytdl.util.Extensions.toBitmap
import java.io.File
@ -643,36 +642,6 @@ class NotificationUtil(var context: Context) {
notificationManager.notify(QUERY_PROCESS_FINISHED_NOTIFICATION_ID, notificationBuilder.build())
}
fun createProcessingDownloads() : Notification{
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
val intent = Intent(context, ProcessDownloadsInBackgroundService::class.java)
intent.putExtra("binding", true)
intent.putExtra("cancel", true)
val cancelNotificationPendingIntent = PendingIntent.getService(
context,
System.currentTimeMillis().toInt(),
intent,
PendingIntent.FLAG_IMMUTABLE
)
notificationBuilder
.setContentTitle(resources.getString(R.string.processing))
.setSmallIcon(R.drawable.ic_app_icon)
.setLargeIcon(
BitmapFactory.decodeResource(
resources,
R.drawable.ic_app_icon
)
)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions()
.addAction(0, resources.getString(R.string.cancel), cancelNotificationPendingIntent)
return notificationBuilder.build()
}
companion object {
const val DOWNLOAD_SERVICE_CHANNEL_ID = "1"
const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2"

View file

@ -25,6 +25,7 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_marginStart="50dp"
android:layout_marginTop="10dp"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputLayout
@ -32,6 +33,7 @@
style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
android:layout_width="70dp"
android:layout_height="wrap_content"
app:errorEnabled="true"
android:layout_marginEnd="10dp"
android:hint="@string/start">
@ -48,6 +50,7 @@
android:layout_width="70dp"
android:layout_height="wrap_content"
android:hint="@string/end"
app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/colon">

View file

@ -391,8 +391,7 @@
<string name="minute">Phút</string>
<string name="second">Giây</string>
<string name="milliseconds">Mili-giây</string>
<string name="format_importance_video">Định dạng theo thứ tự quan trọng (Video)</string>
<string name="format_importance_audio">Định dạng theo thứ tự quan trọng (Âm thanh)</string>
<string name="format_importance">Định dạng theo thứ tự quan trọng</string>
<string name="format_importance_note">Điều chỉnh định dạng theo tầm quan trọng của phần tử. Thứ tự này sẽ được sử dụng khi ứng dụng tìm nạp các định dạng trong thẻ tải xuống và tự động chọn định dạng!</string>
<string name="no_audio">Video không âm thanh</string>
<string name="enable_alarm_permission">Bạn cần cho phép quyền SCHEDULE_EXACT_ALARM trong cài đặt ứng dụng.</string>

View file

@ -62,6 +62,7 @@
app:defaultValue="false"
android:icon="@drawable/baseline_layers_24"
android:key="display_over_apps"
app:isPreferenceVisible="false"
app:summary="@string/display_over_apps_summary"
app:title="@string/display_over_apps" />

View file

@ -0,0 +1,8 @@
## Shortest changelog ever
- Fixed multiple download card issues. Processing them in the background was not a good idea for most users as it was confusing and users thought it wasnt doing anything. It led to duplicate downloads
Usually playlist downloads dont really have 4000 items in them so background. If you have one, just enable quick download and consider the whole playlist as a single item. :)
- Some users had issues with the display over apps perm. Since that perm ended up not doing anything to the revanced issue, i removed it
Had to push this out since multiple download card was almost broken