From 33de21bce9590b5095bebb80c3b5b6e50caf11e0 Mon Sep 17 00:00:00 2001 From: zaednasr <75589932+zaednasr@users.noreply.github.com> Date: Sun, 26 May 2024 20:53:01 +0200 Subject: [PATCH] 1.7.6.1-beta --- app/build.gradle | 3 +- .../ytdl/database/dao/DownloadDao.kt | 58 ++-- .../database/repository/DownloadRepository.kt | 29 +- .../database/repository/ResultRepository.kt | 2 +- .../database/viewmodel/DownloadViewModel.kt | 304 ++++++++---------- .../viewmodel/SharedDownloadViewModel.kt | 116 ++++--- .../ProcessDownloadsInBackgroundService.kt | 75 +++-- .../com/deniscerri/ytdl/ui/HomeFragment.kt | 24 +- .../ytdl/ui/adapter/ActiveDownloadAdapter.kt | 28 +- .../adapter/ActiveDownloadMinifiedAdapter.kt | 5 +- .../deniscerri/ytdl/ui/adapter/HomeAdapter.kt | 2 +- .../downloadcard/CutVideoBottomSheetDialog.kt | 3 + .../ui/downloadcard/DownloadAudioFragment.kt | 16 +- .../downloadcard/DownloadBottomSheetDialog.kt | 1 + .../DownloadMultipleBottomSheetDialog.kt | 242 +++++++------- .../ui/downloadcard/DownloadVideoFragment.kt | 16 +- .../FormatSelectionBottomSheetDialog.kt | 2 + .../downloadcard/SelectPlaylistItemsDialog.kt | 6 +- .../ui/downloads/ActiveDownloadsFragment.kt | 209 +++++------- .../ui/downloads/DownloadQueueMainFragment.kt | 27 +- .../ui/downloads/QueuedDownloadsFragment.kt | 26 +- .../downloadLogs/DownloadLogListFragment.kt | 1 + .../more/settings/UpdateSettingsFragment.kt | 21 +- .../terminal/TerminalDownloadsListFragment.kt | 50 +-- .../ytdl/ui/more/terminal/TerminalFragment.kt | 5 + .../com/deniscerri/ytdl/util/CrashListener.kt | 7 +- .../java/com/deniscerri/ytdl/util/InfoUtil.kt | 28 +- .../deniscerri/ytdl/util/NotificationUtil.kt | 22 +- .../java/com/deniscerri/ytdl/util/UiUtil.kt | 40 ++- .../deniscerri/ytdl/work/DownloadWorker.kt | 94 +++--- .../ytdl/work/ObserveSourceWorker.kt | 2 +- .../ytdl/work/TerminalDownloadWorker.kt | 12 +- .../main/res/layout-land/fragment_home.xml | 71 ++-- .../res/layout-sw600dp-land/fragment_home.xml | 259 ++++++++------- .../main/res/layout-sw600dp/fragment_home.xml | 203 +++++++----- .../res/layout-sw950dp-land/fragment_home.xml | 299 +++++++++-------- .../main/res/layout/active_download_card.xml | 17 + app/src/main/res/layout/cut_video_sheet.xml | 6 +- .../layout/download_multiple_bottom_sheet.xml | 52 ++- app/src/main/res/layout/fragment_home.xml | 47 ++- .../main/res/layout/result_card_shimmer.xml | 204 ++++++------ .../main/res/layout/select_playlist_items.xml | 51 +-- app/src/main/res/values-sq-rAL/strings.xml | 20 ++ app/src/main/res/values/arrays.xml | 2 + app/src/main/res/values/strings.xml | 2 +- app/src/main/res/values/styles.xml | 1 - .../main/res/xml/downloading_preferences.xml | 2 +- app/src/main/res/xml/folders_preference.xml | 4 +- .../main/res/xml/processing_preferences.xml | 4 +- 49 files changed, 1537 insertions(+), 1183 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c59aee13..789e537e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ def properties = new Properties() def versionMajor = 1 def versionMinor = 7 def versionPatch = 6 -def versionBuild = 0 // bump for dogfood builds, public betas, etc. +def versionBuild = 1 // bump for dogfood builds, public betas, etc. def isBeta = true def versionExt = isBeta ? ".${versionBuild}-beta" : "" @@ -195,4 +195,5 @@ dependencies { implementation 'com.google.accompanist:accompanist-webview:0.30.1' implementation 'androidx.compose.material3:material3-android:1.2.1' implementation "io.noties.markwon:core:4.6.2" + implementation("org.greenrobot:eventbus:3.3.1") } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt index 0b4ad519..6d9a7394 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt @@ -21,7 +21,7 @@ interface DownloadDao { @Query("SELECT * FROM downloads ORDER BY status") fun getAllDownloads() : PagingSource - @Query("SELECT * FROM downloads WHERE status in ('Active', 'ActivePaused')") + @Query("SELECT * FROM downloads WHERE status='Active'") fun getActiveDownloads() : Flow> @Query("SELECT * FROM downloads WHERE status = 'Processing'") @@ -38,12 +38,9 @@ interface DownloadDao { @Query(""" - SELECT COUNT(*) FROM downloads WHERE status = 'Processing' - UNION - SELECT COUNT(*) FROM downloads WHERE status ='Processing' AND type = - (SELECT type from downloads WHERE status = 'Processing' ORDER BY id LIMIT 1) + SELECT DISTINCT type from downloads where status = 'Processing' and id in (:ids) """) - fun getProcessingDownloadsCountByType() : List + fun getProcessingDownloadTypes(ids: List) : List @Query("UPDATE downloads set status = 'Processing' WHERE id in (:ids)") @@ -53,7 +50,7 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status = 'Processing' ORDER BY id LIMIT 1") fun getFirstProcessingDownload() : DownloadItem - @Query("SELECT * FROM downloads WHERE status in('Active','ActivePaused')") + @Query("SELECT * FROM downloads WHERE status='Active'") fun getActiveAndPausedDownloadsList() : List @Query("SELECT * FROM downloads WHERE status = 'Processing'") @@ -62,32 +59,31 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Active'") suspend fun getActiveDownloadsList() : List - @Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused', 'Scheduled')") + @Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')") fun getActiveAndQueuedDownloadsList() : List + @Query("UPDATE downloads SET status='Queued' where status in ('Active', 'QueuedPaused', 'ActivePaused')") + suspend fun resetActivePausedItems() - @Query("SELECT id FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused')") + @Query("SELECT id FROM downloads WHERE status in('Active','Queued')") fun getActiveAndQueuedDownloadIDs() : List - @Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused')") + @Query("SELECT * FROM downloads WHERE status in('Active','Queued')") fun getActiveAndQueuedDownloads() : Flow> @RewriteQueriesToDropUnusedColumns - @Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY id") + @Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY id") fun getQueuedDownloads() : PagingSource - @Query("SELECT format FROM downloads WHERE status in('Queued','QueuedPaused')") + @Query("SELECT format FROM downloads WHERE status='Queued'") fun getSelectedFormatFromQueued() : List @Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status in ('Queued', 'Scheduled') ORDER BY downloadStartTime, id LIMIT 20") fun getQueuedScheduledDownloadsUntil(currentTime: Long) : Flow> - @Query("SELECT * FROM downloads WHERE status in ('Queued','QueuedPaused','ActivePaused') ORDER BY downloadStartTime, id") - fun getQueuedAndPausedDownloads() : Flow> - - @Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id") + @Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id") fun getQueuedDownloadsList() : List - @Query("SELECT id FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY id") + @Query("SELECT id FROM downloads WHERE status='Queued' ORDER BY id") fun getQueuedDownloadsListIDs() : List @RewriteQueriesToDropUnusedColumns @@ -133,22 +129,6 @@ interface DownloadDao { @Query("SELECT status FROM downloads WHERE id=:id") fun checkStatus(id: Long) : DownloadRepository.Status? - @Query("UPDATE downloads " + - "SET status = CASE " + - " WHEN status = 'Active' THEN 'ActivePaused' " + - " WHEN status = 'Queued' THEN 'QueuedPaused' " + - " ELSE status " + - " END;") - fun pauseActiveAndQueued() - - @Query("UPDATE downloads " + - "SET status = CASE " + - " WHEN status = 'ActivePaused' THEN 'Active' " + - " WHEN status = 'QueuedPaused' THEN 'Queued' " + - " ELSE status " + - " END;") - fun unPauseActiveAndQueued() - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(item: DownloadItem) : Long @@ -179,7 +159,7 @@ interface DownloadDao { @Query("DELETE FROM downloads WHERE id in (:list)") suspend fun deleteAllWithIDs(list: List) - @Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','QueuedPaused','Active','ActivePaused', 'Scheduled')") + @Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','Active', 'Scheduled')") suspend fun cancelActiveQueued() @Query("DELETE FROM downloads WHERE status='Processing' AND id=:id") @@ -188,6 +168,9 @@ interface DownloadDao { @Upsert suspend fun update(item: DownloadItem) + @Query("UPDATE downloads set status=:status where id=:id") + suspend fun setStatus(id: Long, status: String) + @Update suspend fun updateWithoutUpsert(item: DownloadItem) @@ -233,14 +216,11 @@ interface DownloadDao { @Query("UPDATE downloads SET downloadStartTime=0, status='Queued' where id in (:list)") suspend fun resetScheduleTimeForItems(list: List) - @Query("UPDATE downloads SET status='Queued' where status in ('QueuedPaused', 'ActivePaused')") - suspend fun resetPausedItems() - @Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)") suspend fun reQueueDownloadItems(list: List) - @Query("Update downloads SET status='Saved' WHERE status='Processing'") - fun updateProcessingtoSavedStatus() + @Query("Update downloads SET status='Saved' WHERE status='Processing' and id in (:ids)") + fun updateProcessingtoSavedStatus(ids: List) @Transaction suspend fun putAtTopOfTheQueue(existingIDs: List){ diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt index ee49a25d..ed96ef10 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt @@ -26,6 +26,7 @@ import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.work.DownloadWorker import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import java.io.File import java.util.concurrent.TimeUnit @@ -59,17 +60,15 @@ class DownloadRepository(private val downloadDao: DownloadDao) { ) val activeDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active).toListString()) - val activeAndActivePausedDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.ActivePaused).toListString()) - val queuedDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Queued, Status.QueuedPaused).toListString()) - val activeQueuedDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.ActivePaused, Status.Queued, Status.QueuedPaused).toListString()) + val queuedDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Queued).toListString()) + val activeQueuedDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.Queued).toListString()) val cancelledDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Cancelled).toListString()) val erroredDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Error).toListString()) val savedDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Saved).toListString()) - val pausedDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.ActivePaused, Status.QueuedPaused).toListString()) val scheduledDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Scheduled).toListString()) enum class Status { - Active, ActivePaused, Queued, QueuedPaused, Error, Cancelled, Saved, Processing, Scheduled + Active, ActivePaused, Queued, Error, Cancelled, Saved, Processing, Scheduled } suspend fun insert(item: DownloadItem) : Long { @@ -102,9 +101,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) { } - suspend fun setDownloadStatus(item: DownloadItem, status: Status){ - item.status = status.toString() - update(item) + suspend fun setDownloadStatus(id: Long, status: Status){ + downloadDao.setStatus(id, status.toString()) } fun getItemByID(id: Long) : DownloadItem { @@ -184,14 +182,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) { downloadDao.cancelActiveQueued() } - fun pauseDownloads(){ - downloadDao.pauseActiveAndQueued() - } - - fun unPauseDownloads(){ - downloadDao.unPauseActiveAndQueued() - } - fun removeLogID(logID: Long){ downloadDao.removeLogID(logID) } @@ -200,6 +190,13 @@ class DownloadRepository(private val downloadDao: DownloadDao) { downloadDao.removeAllLogID() } + fun getFilteredProcessingDownloads(ids: List) : Flow> { + return downloadDao.getProcessingDownloads() + .map { + it.filter { ids.contains(it.id) } + } + } + @SuppressLint("RestrictedApi") suspend fun startDownloadWorker(queuedItems: List, context: Context, inputData: Data.Builder = Data.Builder()) { diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt index a69e1319..31a854f7 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt @@ -157,7 +157,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co private fun getQueryType(inputQuery: String) : SourceType { var type = SourceType.SEARCH_QUERY - val p = Pattern.compile("((^(https?)://)?(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)") + val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)") val m = p.matcher(inputQuery) if (m.find()) { type = SourceType.YOUTUBE_VIDEO diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt index 13dddb62..990c3055 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt @@ -5,13 +5,10 @@ import android.app.Application import android.content.SharedPreferences import android.content.res.Configuration import android.content.res.Resources -import android.os.Handler -import android.os.Looper -import android.os.Parcelable import android.util.DisplayMetrics -import android.widget.Toast import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import androidx.paging.PagingData @@ -20,7 +17,6 @@ import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager -import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdl.App import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.DBManager @@ -41,21 +37,20 @@ import com.deniscerri.ytdl.ui.downloadcard.FormatTuple import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.InfoUtil -import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker -import com.google.gson.Gson import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.map import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import kotlinx.parcelize.Parcelize import java.io.File import java.util.Locale @@ -77,14 +72,12 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val scheduledDownloads : Flow> val activeDownloadsCount : Flow - val activeAndActivePausedDownloadsCount : Flow val queuedDownloadsCount : Flow val activeQueuedDownloadsCount : Flow val cancelledDownloadsCount : Flow val erroredDownloadsCount : Flow val savedDownloadsCount : Flow val scheduledDownloadsCount : Flow - val pausedDownloadsCount: Flow val alreadyExistsUiState: MutableStateFlow> @@ -130,14 +123,12 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel infoUtil = InfoUtil(application) activeDownloadsCount = repository.activeDownloadsCount - activeAndActivePausedDownloadsCount = repository.activeAndActivePausedDownloadsCount queuedDownloadsCount = repository.queuedDownloadsCount activeQueuedDownloadsCount = repository.activeQueuedDownloadsCount cancelledDownloadsCount = repository.cancelledDownloadsCount erroredDownloadsCount = repository.erroredDownloadsCount savedDownloadsCount = repository.savedDownloadsCount scheduledDownloadsCount = repository.scheduledDownloadsCount - pausedDownloadsCount = repository.pausedDownloadsCount allDownloads = repository.allDownloads.flow queuedDownloads = repository.queuedDownloads.flow @@ -294,8 +285,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val cropThumb = sharedPreferences.getBoolean("crop_thumbnail", false) val customFileNameTemplate = when(historyItem.type) { - Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)s - %(title)s") - Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s") + Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)30B - %(title)120B") + Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)30B - %(title)120B") else -> "" } @@ -371,78 +362,57 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } data class ProcessingItemsJob( - var jobID: Long = 0, var job: Job? = null, - var itemType: String, - var itemIDs: List + var originItemType: String, + var originItemIDs: List, + var processingDownloadItemIDs: MutableList = mutableListOf() ) - val loadingProcessingItems: MutableStateFlow = MutableStateFlow(false) - var loadingProcessingDownloadsJobs: MutableStateFlow> = MutableStateFlow(listOf()) - private fun cancelAllLoadingProcessingDownloads() { - loadingProcessingDownloadsJobs.value.onEach { - it.job?.cancel(CancellationException()) - } + var processingItemsJob: MutableLiveData = MutableLiveData(null) + var _filteredProcessingItems = MutableStateFlow>(listOf()) + var filteredProcessingItems: StateFlow> = _filteredProcessingItems + private fun updateProcessingJobData(item: ProcessingItemsJob) = viewModelScope.launch { + processingItemsJob.postValue(item) } - fun getLoadingProcessingDownloadJob(jobID: Long) : ProcessingItemsJob? { - return loadingProcessingDownloadsJobs.value.firstOrNull { it.jobID == jobID } - } - - fun cancelLoadingProcessingDownloads(jobID: Long) { - loadingProcessingDownloadsJobs.value.firstOrNull {it.jobID == jobID }?.apply { - this.job?.cancel(CancellationException()) - } - } - - fun turnDownloadItemsToProcessingDownloads(itemIDs: List) : Long { - val jobID = System.currentTimeMillis() + fun turnDownloadItemsToProcessingDownloads(itemIDs: List) = viewModelScope.launch(Dispatchers.IO){ + processingItemsJob.postValue(ProcessingItemsJob(null, DownloadItem::class.java.toString(), itemIDs)) val job = viewModelScope.launch(Dispatchers.IO) { - val insertedIDs = mutableListOf() try { - loadingProcessingItems.emit(true) - itemIDs.chunked(100).forEach { ids -> val items = repository.getAllItemsByIDs(ids) - items.apply { - if (!isActive) { - throw CancellationException() - } - this.forEach { - it.id = 0 - it.status = DownloadRepository.Status.Processing.toString() - insertedIDs.add(repository.insert(it)) - } + if (!isActive) { + throw CancellationException() + } + items.forEach { + it.id = 0 + it.status = DownloadRepository.Status.Processing.toString() + } + + processingItemsJob.value?.apply { + processingDownloadItemIDs.addAll(repository.insertAll(items)) + updateProcessingJobData(this) } } - loadingProcessingItems.emit(false) } catch (e: Exception) { - repository.deleteAllWithIDs(insertedIDs) - loadingProcessingItems.emit(false) + processingItemsJob.value?.apply { + this.processingDownloadItemIDs.chunked(100).forEach { + repository.deleteAllWithIDs(it) + } + } + processingItemsJob.postValue(null) } } - - viewModelScope.launch(Dispatchers.IO) { - val currentJobs = loadingProcessingDownloadsJobs.value.toMutableList() - currentJobs.add( - ProcessingItemsJob(jobID, job, DownloadItem::class.java.toString(), itemIDs) - ) - loadingProcessingDownloadsJobs.emit(currentJobs) - } - - return jobID + processingItemsJob.postValue(ProcessingItemsJob(job, DownloadItem::class.java.toString(), itemIDs)) } - fun turnResultItemsToProcessingDownloads(itemIds: List, downloadNow: Boolean = false) : Long { - val jobID = System.currentTimeMillis() - val job = viewModelScope.launch(Dispatchers.IO) { - val insertedIds = mutableListOf() + fun turnResultItemsToProcessingDownloads(itemIDs: List, downloadNow: Boolean = false) = viewModelScope.launch(Dispatchers.IO) { + processingItemsJob.postValue(ProcessingItemsJob(null, ResultItem::class.java.toString(), itemIDs)) + val job = viewModelScope.launch(Dispatchers.IO) { try { - loadingProcessingItems.emit(true) - - itemIds.chunked(100).forEach { ids -> + itemIDs.chunked(100).forEach { ids -> val items = resultRepository.getAllByIDs(ids) val downloadItems = items.map { val preferredType = getDownloadType(url = it.url).toString() @@ -462,41 +432,29 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } queueDownloads(downloadItems) }else{ - downloadItems.forEach { - insertedIds.add(repository.insert(it)) + processingItemsJob.value?.apply { + processingDownloadItemIDs.addAll(repository.insertAll(downloadItems)) + updateProcessingJobData(this) } } } - loadingProcessingItems.emit(false) }catch (e: Exception) { - repository.deleteAllWithIDs(insertedIds) - loadingProcessingItems.emit(false) + processingItemsJob.value?.apply { + this.processingDownloadItemIDs.chunked(100).forEach { + repository.deleteAllWithIDs(it) + } + } + processingItemsJob.postValue(null) } } - viewModelScope.launch(Dispatchers.IO) { - val currentJobs = loadingProcessingDownloadsJobs.value.toMutableList() - currentJobs.add( - ProcessingItemsJob(jobID, job, ResultItem::class.java.toString(), itemIds) - ) - loadingProcessingDownloadsJobs.emit(currentJobs) - } - - return jobID + processingItemsJob.postValue(ProcessingItemsJob(job, ResultItem::class.java.toString(), itemIDs)) } fun insert(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){ repository.insert(item) } - fun pauseDownloads() = viewModelScope.launch(Dispatchers.IO){ - repository.pauseDownloads() - } - - fun unPauseDownloads() = viewModelScope.launch(Dispatchers.IO){ - repository.unPauseDownloads() - } - fun insertAll(items: List)= viewModelScope.launch(Dispatchers.IO){ items.forEach{ repository.insert(it) @@ -540,7 +498,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) { - cancelAllLoadingProcessingDownloads() + processingItemsJob.value?.apply { this.job?.cancel(CancellationException()) } repository.cancelActiveQueued() } @@ -563,8 +521,12 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel return repository.getActiveDownloads() } - fun getActiveAndQueuedDownloads() : List{ - return repository.getActiveAndQueuedDownloads() + fun getActiveDownloadsCount() : Int { + return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused).toListString()) + } + + fun getQueuedDownloadsCount() : Int { + return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Queued).toListString()) } fun getActiveAndQueuedDownloadIDs() : List{ @@ -573,10 +535,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel suspend fun resetScheduleTimeForItemsAndStartDownload(items: List) = CoroutineScope(Dispatchers.IO).launch { dbManager.downloadDao.resetScheduleTimeForItems(items) - dbManager.downloadDao.resetPausedItems() repository.startDownloadWorker(emptyList(), application) } + suspend fun resetActivePaused() { + dbManager.downloadDao.resetActivePausedItems() + } + suspend fun startDownloadWorker(list: List){ repository.startDownloadWorker(list, application) } @@ -664,36 +629,42 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel return dbManager.downloadDao.getDownloadIDsNotPresentInList(items.ifEmpty { listOf(-1L) }, status.map { it.toString() }) } - fun moveProcessingToSavedCategory(){ - dao.updateProcessingtoSavedStatus() + fun moveProcessingToSavedCategory(ids: List){ + ids.chunked(100).forEach { + dao.updateProcessingtoSavedStatus(it) + } } - suspend fun updateProcessingFormat(selectedFormats: List): List { - val items = repository.getProcessingDownloads() - items.forEachIndexed { index, i -> - selectedFormats[index].format?.apply { - i.format = this + suspend fun updateProcessingFormat(ids: List, selectedFormats: List): List { + 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) } - if (i.type == Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) } - repository.update(i) - } - return items.map { it.format.filesize } + items.map { itm -> itm.format.filesize } + }.flatten() } - suspend fun updateProcessingCommandFormat(format: Format){ - val items = repository.getProcessingDownloads() - items.forEach { i -> - i.format = format - repository.update(i) + suspend fun updateProcessingCommandFormat(ids: List, format: Format){ + ids.chunked(100).forEach { + repository.getAllItemsByIDs(it).forEach { i -> + i.format = format + repository.update(i) + } } } - suspend fun updateProcessingDownloadPath(path: String){ - val items = repository.getProcessingDownloads() - items.forEach { i -> - i.downloadPath = path - repository.update(i) + suspend fun updateProcessingDownloadPath(ids: List, path: String){ + ids.chunked(100).forEach { + repository.getAllItemsByIDs(it).forEach { i -> + i.downloadPath = path + repository.update(i) + } } } @@ -706,66 +677,73 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } - suspend fun updateProcessingAllFormats(formatCollection: List>) { - 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]) + suspend fun updateProcessingAllFormats(ids: List, formatCollection: List>) { + 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]) + } } - } - i.format = getFormat(i.allFormats, i.type) - kotlin.runCatching { - dbManager.resultDao.getResultByURL(i.url)?.apply { - this.formats = formatCollection[index].toMutableList() - dbManager.resultDao.update(this) + 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) } - repository.update(i) } + } - suspend fun continueUpdatingFormatsOnBackground(){ - dao.getProcessingDownloadsList().apply { - this.forEach { + suspend fun continueUpdatingFormatsOnBackground(itemIDs: List){ + itemIDs.chunked(100).forEach {list -> + repository.getAllItemsByIDs(list).onEach { it.status = DownloadRepository.Status.Saved.toString() repository.update(it) } - - val ids = this.map { it.id } - val id = System.currentTimeMillis().toInt() - val workRequest = OneTimeWorkRequestBuilder() - .setInputData( - Data.Builder() - .putLongArray("ids", ids.toLongArray()) - .putInt("id", id) - .build()) - .addTag("updateFormats") - .build() - val context = App.instance - WorkManager.getInstance(context).enqueueUniqueWork( - id.toString(), - ExistingWorkPolicy.REPLACE, - workRequest - ) } + + val id = System.currentTimeMillis().toInt() + val workRequest = OneTimeWorkRequestBuilder() + .setInputData( + Data.Builder() + .putLongArray("ids", itemIDs.toLongArray()) + .putInt("id", id) + .build()) + .addTag("updateFormats") + .build() + val context = App.instance + WorkManager.getInstance(context).enqueueUniqueWork( + id.toString(), + ExistingWorkPolicy.REPLACE, + workRequest + ) + } - suspend fun updateProcessingType(newType: Type) { - repository.getProcessingDownloads().apply { - val new = switchDownloadType(this, newType) - new.forEach { - repository.update(it) + suspend fun updateProcessingType(ids: List, newType: Type) { + ids.chunked(100).forEach { _ -> + repository.getAllItemsByIDs(ids).apply { + val new = switchDownloadType(this, newType) + new.forEach { + repository.update(it) + } } } } - fun checkIfAllProcessingItemsHaveSameType() : Pair { - val counts = dao.getProcessingDownloadsCountByType() - val sameType = counts.size == 1 || counts[0] == counts[1] - val first = dao.getFirstProcessingDownload() - return Pair(sameType, first.type) + fun checkIfAllProcessingItemsHaveSameType(ids: List) : Pair { + val types = mutableSetOf() + ids.chunked(100).forEach { + types.addAll(dao.getProcessingDownloadTypes(it)) + } + + return Pair(types.size == 1, Type.valueOf(types.first())) } @@ -775,6 +753,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val first = dao.getFirstProcessingDownload() } + suspend fun updateToStatus(id: Long, status: DownloadRepository.Status) { + repository.setDownloadStatus(id, status) + } + fun getURLsByStatus(list: List) : List { return dao.getURLsByStatus(list.map { it.toString() }) } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SharedDownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SharedDownloadViewModel.kt index 533ab203..68d546f5 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SharedDownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SharedDownloadViewModel.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize +import okhttp3.internal.immutableListOf import java.io.File import java.util.Locale @@ -282,16 +283,32 @@ class SharedDownloadViewModel(private val context: Context) { private fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> { val requirements: MutableList<(Format) -> Int> = mutableListOf() - requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) 10 else 0} - sharedPreferences.getString("audio_language", "")?.apply { - if (this.isNotBlank()){ - requirements.add { it: Format -> if (it.lang?.contains(this) == true) 3 else 0 } + val itemValues = resources.getStringArray(R.array.format_importance_audio_values).toSet() + val prefAudio = sharedPreferences.getString("format_importance_audio", itemValues.joinToString(","))!! + + prefAudio.split(",").forEachIndexed { idx, s -> + var importance = (itemValues.size - idx) * 10 + when(s) { + "id" -> { + requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) importance else 0} + } + "language" -> { + sharedPreferences.getString("audio_language", "")?.apply { + if (this.isNotBlank()){ + requirements.add { it: Format -> if (it.lang?.contains(this) == true) importance else 0 } + } + } + } + "codec" -> { + requirements.add {it: Format -> if ("^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)) importance else 0} + } + "container" -> { + requirements.add {it: Format -> if (it.container == audioContainer) importance else 0 } + } } } - requirements.add {it: Format -> if ("^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)) 2 else 0} - requirements.add {it: Format -> if (it.container == audioContainer) 1 else 0 } return requirements } @@ -299,42 +316,61 @@ class SharedDownloadViewModel(private val context: Context) { @SuppressLint("RestrictedApi") fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> { val requirements: MutableList<(Format) -> Int> = mutableListOf() - //format id - requirements.add { it: Format -> if (formatIDPreference.contains(it.format_id)) 20 else 0 } - //resolutions - context.getStringArray(R.array.video_formats_values) - .filter { it.contains("_") } - .map{ it.split("_")[0].dropLast(1) - }.toMutableList().apply { - when(videoQualityPreference) { - "worst" -> { - requirements.add { it: Format -> if (it.format_note.contains("worst", ignoreCase = true)) (15) else 0 } - } - "best" -> { - requirements.add { it: Format -> if (it.format_note.contains("best", ignoreCase = true)) (15) else 0 } - } - else -> { - val preferenceIndex = this.indexOfFirst { videoQualityPreference.contains(it) } - val preference = this[preferenceIndex] - for(i in 0..preferenceIndex){ - removeAt(0) - } - add(0, preference) - forEachIndexed { index, res -> - requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (15 - index - 1) else 0 } + + val itemValues = resources.getStringArray(R.array.format_importance_video_values).toSet() + val prefVideo = sharedPreferences.getString("format_importance_video", itemValues.joinToString(","))!! + + prefVideo.split(",").forEachIndexed { idx, s -> + val importance = (itemValues.size - idx) * 10 + + when(s) { + "id" -> { + requirements.add { it: Format -> if (formatIDPreference.contains(it.format_id)) importance else 0 } + } + "resolution" -> { + context.getStringArray(R.array.video_formats_values) + .filter { it.contains("_") } + .map{ it.split("_")[0].dropLast(1) + }.toMutableList().apply { + when(videoQualityPreference) { + "worst" -> { + requirements.add { it: Format -> if (it.format_note.contains("worst", ignoreCase = true)) (importance) else 0 } + } + "best" -> { + requirements.add { it: Format -> if (it.format_note.contains("best", ignoreCase = true)) (importance) else 0 } + } + else -> { + val preferenceIndex = this.indexOfFirst { videoQualityPreference.contains(it) } + val preference = this[preferenceIndex] + for(i in 0..preferenceIndex){ + removeAt(0) + } + add(0, preference) + forEachIndexed { index, res -> + requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (importance - index - 1) else 0 } + } + } + } + } + } + "codec" -> { + requirements.add { it: Format -> if ("^(${videoCodec})(.+)?$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)) importance else 0 } + } + "no_audio" -> { + requirements.add { it: Format -> if (it.acodec == "none" || it.acodec == "") importance else 0 } + } + "container" -> { + requirements.add { it: Format -> + if (videoContainer == "mp4") + if (it.container.equals("mpeg_4", true)) importance else 0 + else + if (it.container.equals(videoContainer, true)) importance else 0 } } - } - requirements.add { it: Format -> if ("^(${videoCodec})(.+)?$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)) 5 else 0 } - requirements.add { it: Format -> if (it.acodec == "none" || it.acodec == "") 1 else 0 } - requirements.add { it: Format -> - if (videoContainer == "mp4") - if (it.container.equals("mpeg_4", true)) 1 else 0 - else - if (it.container.equals(videoContainer, true)) 1 else 0 } + return requirements } @@ -445,7 +481,7 @@ class SharedDownloadViewModel(private val context: Context) { val downloadArchive = runCatching { File(FileUtil.getDownloadArchivePath(context)).useLines { it.toList() } }.getOrElse { listOf() } .map { it.split(" ")[1] } items.forEach { - if (! listOf(DownloadRepository.Status.ActivePaused, DownloadRepository.Status.Scheduled).toListString().contains(it.status)) + if (it.status != DownloadRepository.Status.Scheduled.toString()) it.status = DownloadRepository.Status.Queued.toString() var alreadyExists = false @@ -584,7 +620,9 @@ class SharedDownloadViewModel(private val context: Context) { } }else{ if (queuedItems.isNotEmpty()){ - repository.startDownloadWorker(queuedItems, context) + if (!sharedPreferences.getBoolean("paused_downloads", false)) { + repository.startDownloadWorker(queuedItems, context) + } if(!useScheduler){ queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach { diff --git a/app/src/main/java/com/deniscerri/ytdl/services/ProcessDownloadsInBackgroundService.kt b/app/src/main/java/com/deniscerri/ytdl/services/ProcessDownloadsInBackgroundService.kt index cff70e42..da190925 100644 --- a/app/src/main/java/com/deniscerri/ytdl/services/ProcessDownloadsInBackgroundService.kt +++ b/app/src/main/java/com/deniscerri/ytdl/services/ProcessDownloadsInBackgroundService.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -43,39 +44,25 @@ class ProcessDownloadsInBackgroundService : Service() { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + + val binding = intent.getBooleanExtra("binding", false) + if (binding) 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") ?: longArrayOf() - val jobData = DownloadViewModel.ProcessingItemsJob( - itemType = itemType, - itemIDs = itemIDs.toList() - ) + + val itemType = intent.getStringExtra("itemType")!! + val itemIDs = intent.getLongArrayExtra("itemIDs")!!.toList() + val processingItemIDs = intent.getLongArrayExtra("processingItemIDs")!!.toList() val timeInMillis = intent.getLongExtra("timeInMillis", 0) - CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { - runJob(jobData, timeInMillis) - } - return super.onStartCommand(intent, flags, startId) - } + val processingFinished = intent.getBooleanExtra("processingFinished", true) - override fun onBind(intent: Intent): IBinder { - return binder - } - - private fun DownloadItem.setAsScheduling(timeInMillis: Long) { - status = DownloadRepository.Status.Scheduled.toString() - downloadStartTime = timeInMillis - } - - private suspend fun runJob(jobData: DownloadViewModel.ProcessingItemsJob, timeInMillis: Long = 0) { val job = CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { - if (jobData.itemType != "") { - val itemIDS = jobData.itemIDs - val processingType = jobData.itemType - when(processingType) { + if (!processingFinished) { + when(itemType) { ResultItem::class.java.toString() -> { - itemIDS.chunked(100).map { ids -> + itemIDs.chunked(100).map { ids -> resultRepository.getAllByIDs(ids).map { downloadViewModel.createDownloadItemFromResult( result = it, givenType = DownloadViewModel.Type.valueOf( @@ -88,33 +75,42 @@ class ProcessDownloadsInBackgroundService : Service() { it.setAsScheduling(timeInMillis) } } - downloadViewModel.queueDownloads(this) + if (isActive){ + downloadViewModel.queueDownloads(this) + } } } } DownloadItem::class.java.toString() -> { - itemIDS.chunked(100).map { ids -> + itemIDs.chunked(100).map { ids -> repository.getAllItemsByIDs(ids).apply { if (timeInMillis > 0) { this.forEach { it.setAsScheduling(timeInMillis) } } - downloadViewModel.queueDownloads(this) + if (isActive){ + downloadViewModel.queueDownloads(this) + } } } } } }else { - repository.getProcessingDownloads().apply { - if (timeInMillis > 0){ - this.forEach { - it.setAsScheduling(timeInMillis) + repository.getAllItemsByIDs(processingItemIDs).apply { + this.chunked(100).map { + if (timeInMillis > 0){ + this.forEach { d -> + d.setAsScheduling(timeInMillis) + } + } + + if (isActive){ + downloadViewModel.queueDownloads(it) } } - downloadViewModel.queueDownloads(this) } } } @@ -126,6 +122,17 @@ class ProcessDownloadsInBackgroundService : Service() { } } 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(){ diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt index 06d6a4a3..b1bda6b3 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt @@ -51,11 +51,13 @@ import com.facebook.shimmer.ShimmerFrameLayout import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.button.MaterialButton +import com.google.android.material.card.MaterialCardView import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.search.SearchBar import com.google.android.material.search.SearchView import kotlinx.coroutines.CoroutineScope @@ -176,12 +178,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti searchSuggestionsRecyclerView?.adapter = searchSuggestionsAdapter searchSuggestionsRecyclerView?.itemAnimator = null + val progressBar = view.findViewById(R.id.progress) resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel.items.observe(requireActivity()) { kotlin.runCatching { homeAdapter!!.submitList(it) resultsList = it + progressBar.isVisible = loadingItems && resultsList!!.isNotEmpty() if(resultViewModel.repository.itemCount.value > 1 || resultViewModel.repository.itemCount.value == -1){ if (it.size > 1 && it[0].playlistTitle.isNotEmpty() && !loadingItems){ downloadAllFab!!.visibility = VISIBLE @@ -252,6 +256,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti } loadingItems = res.processing + progressBar.isVisible = loadingItems && resultsList!!.isNotEmpty() if (res.processing){ recyclerView?.setPadding(0,0,0,0) shimmerCards!!.startShimmer() @@ -303,11 +308,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti arguments?.remove("showDownloadsWithUpdatedFormats") CoroutineScope(Dispatchers.IO).launch { val ids = arguments?.getLongArray("downloadIds") ?: return@launch - val jobID = downloadViewModel.turnDownloadItemsToProcessingDownloads(ids.toList()) + downloadViewModel.turnDownloadItemsToProcessingDownloads(ids.toList()) withContext(Dispatchers.Main){ - findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf( - Pair("processingDownloadsJobID", jobID) - )) + findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2) } } } @@ -453,6 +456,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti searchBar!!.setOnMenuItemClickListener { m: MenuItem -> when (m.itemId) { R.id.delete_results -> { + resultsList = listOf() lifecycleScope.launch { withContext(Dispatchers.IO){ resultViewModel.cancelParsingQueries() @@ -733,11 +737,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti if (viewIdName.isNotEmpty()) { if (viewIdName == "downloadAll") { val showDownloadCard = sharedPreferences!!.getBoolean("download_card", true) - val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(resultsList!!.map { it!!.id }, downloadNow = !showDownloadCard) + downloadViewModel.turnResultItemsToProcessingDownloads(resultsList!!.map { it!!.id }, downloadNow = !showDownloadCard) if (showDownloadCard){ - findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf( - Pair("processingDownloadsJobID", jobID) - )) + findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2) } } } @@ -807,11 +809,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti downloadViewModel.getDownloadType(url = selectedObjects[0].url) ) }else{ - val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(selectedObjects.map { it.id }, downloadNow = !showDownloadCard) + downloadViewModel.turnResultItemsToProcessingDownloads(selectedObjects.map { it.id }, downloadNow = !showDownloadCard) if (showDownloadCard){ - findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf( - Pair("processingDownloadsJobID", jobID) - )) + findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2) } } clearCheckedItems() diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadAdapter.kt index b796a065..d1ea6eee 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadAdapter.kt @@ -11,6 +11,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.preference.PreferenceManager import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil @@ -121,22 +122,27 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null) cancelButton.setOnClickListener {onItemClickListener.onCancelClick(item.id)} + val activePaused = item.status == DownloadRepository.Status.ActivePaused.toString() + val resumeButton = card.findViewById(R.id.active_download_resume) + resumeButton.isVisible = activePaused + if (resumeButton.hasOnClickListeners()) resumeButton.setOnClickListener(null) + resumeButton.setOnClickListener { + resumeButton.isVisible = false + onItemClickListener.onResumeClick(item.id) + } - when(DownloadRepository.Status.valueOf(item.status)){ - DownloadRepository.Status.Active -> { - progressBar.isIndeterminate = true - cancelButton.isEnabled = true - } - DownloadRepository.Status.ActivePaused -> { - progressBar.isIndeterminate = false - cancelButton.isEnabled = true - output.text = activity.getString(R.string.exo_download_paused) - } - else -> {} + if (sharedPreferences.getBoolean("paused_downloads", false) || activePaused) { + progressBar.isIndeterminate = false + cancelButton.isEnabled = true + output.text = activity.getString(R.string.exo_download_paused) + }else{ + progressBar.isIndeterminate = true + cancelButton.isEnabled = true } } interface OnItemClickListener { fun onCancelClick(itemID: Long) + fun onResumeClick(itemID: Long) fun onOutputClick(item: DownloadItem) } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt index d585b7de..68eb1e23 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt @@ -125,6 +125,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac else fileSize.text = fileSizeReadable val menu = card.findViewById(R.id.options) + val paused = sharedPreferences.getBoolean("paused_downloads", false) || item.status == DownloadRepository.Status.ActivePaused.toString() menu.setOnClickListener { val popup = PopupMenu(activity, it) popup.menuInflater.inflate(R.menu.active_downloads_minified, popup.menu) @@ -133,7 +134,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac val pause = popup.menu[0] val resume = popup.menu[1] - if (item.status == DownloadRepository.Status.ActivePaused.toString()){ + if (paused){ pause.isVisible = false resume.isVisible = true }else{ @@ -165,7 +166,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac } - progressBar.isIndeterminate = item.status != DownloadRepository.Status.ActivePaused.toString() + progressBar.isIndeterminate = !paused card.setOnClickListener { onItemClickListener.onCardClick() diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HomeAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HomeAdapter.kt index e50c7073..0c95021f 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HomeAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HomeAdapter.kt @@ -78,7 +78,7 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity) val author = card.findViewById(R.id.author) author.text = video.author val duration = card.findViewById(R.id.duration) - if (video.duration.isNotEmpty()) { + if (video.duration.isNotEmpty() && video.duration != "-1") { duration.text = video.duration } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/CutVideoBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/CutVideoBottomSheetDialog.kt index ea8d4a64..4f40f349 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/CutVideoBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/CutVideoBottomSheetDialog.kt @@ -7,6 +7,7 @@ import android.content.res.ColorStateList import android.graphics.Color import android.net.Uri import android.os.Bundle +import android.text.method.DigitsKeyListener import android.util.DisplayMetrics import android.view.KeyEvent import android.view.LayoutInflater @@ -144,7 +145,9 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private muteBtn = view.findViewById(R.id.mute) rangeSlider = view.findViewById(R.id.rangeSlider) fromTextInput = view.findViewById(R.id.from_textinput_edittext) + fromTextInput.keyListener = DigitsKeyListener.getInstance("0123456789:.") toTextInput = view.findViewById(R.id.to_textinput_edittext) + toTextInput.keyListener = DigitsKeyListener.getInstance("0123456789:.") cancelBtn = view.findViewById(R.id.cancelButton) okBtn = view.findViewById(R.id.okButton) suggestedChips = view.findViewById(R.id.chapters) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadAudioFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadAudioFragment.kt index aa5cd864..0694d2b2 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadAudioFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadAudioFragment.kt @@ -85,7 +85,21 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private override fun onViewCreated(view: View, savedInstanceState: Bundle?) { lifecycleScope.launch { downloadItem = withContext(Dispatchers.IO) { - if (currentDownloadItem != null){ + if (savedInstanceState?.containsKey("updated") == true) { + downloadItem.apply { + title = resultItem!!.title + author = resultItem!!.author + allFormats = resultItem!!.formats + format = downloadViewModel.getFormat(allFormats, Type.audio) + duration = resultItem!!.duration + playlistIndex = resultItem!!.playlistIndex + playlistURL = resultItem!!.playlistURL + playlistTitle = resultItem!!.playlistTitle + thumb = resultItem!!.thumb + website = resultItem!!.website + url = resultItem!!.url + } + }else if (currentDownloadItem != null){ //object cloning val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java) Gson().fromJson(string, DownloadItem::class.java) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt index d71bd321..ced64510 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt @@ -323,6 +323,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { getAlsoAudioDownloadItem{ audioDownloadItem -> audioDownloadItem.downloadStartTime = it.timeInMillis + audioDownloadItem.status = DownloadRepository.Status.Scheduled.toString() itemsToQueue.add(audioDownloadItem) runBlocking { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt index 8fe5cb0d..d85c5428 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt @@ -51,6 +51,8 @@ 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 import com.google.android.material.bottomsheet.BottomSheetDialog @@ -66,6 +68,7 @@ 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 @@ -73,6 +76,7 @@ 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 { @@ -87,12 +91,17 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure private lateinit var bottomAppBar: BottomAppBar private lateinit var filesize : TextView private lateinit var count : TextView + private lateinit var title: TextView + private lateinit var subtitle: TextView + private lateinit var shimmerTitle: ShimmerFrameLayout + private lateinit var shimmerSubtitle: ShimmerFrameLayout private lateinit var sharedPreferences: SharedPreferences private lateinit var currentDownloadIDs: List - private var processingDownloadsJobID: Long = 0 private var processingItemsCount : Int = 0 + private lateinit var jobData: DownloadViewModel.ProcessingItemsJob + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] @@ -103,7 +112,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure infoUtil = InfoUtil(requireContext()) currentDownloadIDs = arguments?.getLongArray("currentDownloadIDs")?.toList() ?: listOf() - processingDownloadsJobID = arguments?.getLong("processingDownloadsJobID") ?: 0 processingItemsCount = currentDownloadIDs.size } @@ -150,14 +158,76 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure val scheduleBtn = view.findViewById(R.id.bottomsheet_schedule_button) val download = view.findViewById