diff --git a/app/build.gradle b/app/build.gradle index 32e3ba0c..237f29b1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ plugins { def properties = new Properties() def versionMajor = 1 def versionMinor = 7 -def versionPatch = 4 +def versionPatch = 5 def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionExt = "" @@ -144,7 +144,7 @@ dependencies { implementation "androidx.navigation:navigation-ui-ktx:$navVer" implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.test.ext:junit-ktx:1.1.5' - implementation 'androidx.compose.ui:ui-android:1.6.2' + implementation 'androidx.compose.ui:ui-android:1.6.5' testImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer" androidTestImplementation "androidx.test.ext:junit:$androidJunitVer" @@ -196,6 +196,6 @@ dependencies { implementation "com.anggrayudi:storage:1.5.5" implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'com.google.accompanist:accompanist-webview:0.30.1' - implementation 'androidx.compose.material3:material3-android:1.2.0' + implementation 'androidx.compose.material3:material3-android:1.2.1' implementation "io.noties.markwon:core:4.6.2" } 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 d8aa09c4..21a3a870 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 @@ -20,7 +20,7 @@ interface DownloadDao { @Query("SELECT * FROM downloads ORDER BY status") fun getAllDownloads() : PagingSource - @Query("SELECT * FROM downloads WHERE status in ('Active', 'ActivePaused', 'PausedReQueued')") + @Query("SELECT * FROM downloads WHERE status in ('Active', 'ActivePaused')") fun getActiveDownloads() : Flow> @Query("SELECT * FROM downloads WHERE status = 'Processing'") @@ -52,7 +52,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','PausedReQueued')") + @Query("SELECT * FROM downloads WHERE status in('Active','ActivePaused')") fun getActiveAndPausedDownloadsList() : List @Query("SELECT * FROM downloads WHERE status = 'Processing'") @@ -61,32 +61,32 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Active'") suspend fun getActiveDownloadsList() : List - @Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused','PausedReQueued')") + @Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused', 'Scheduled')") fun getActiveAndQueuedDownloadsList() : List - @Query("SELECT id FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused','PausedReQueued')") + @Query("SELECT id FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused')") fun getActiveAndQueuedDownloadIDs() : List - @Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused','PausedReQueued')") + @Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused')") fun getActiveAndQueuedDownloads() : Flow> @RewriteQueriesToDropUnusedColumns - @Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id") + @Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY id") fun getQueuedDownloads() : PagingSource @Query("SELECT format FROM downloads WHERE status in('Queued','QueuedPaused')") fun getSelectedFormatFromQueued() : List - @Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status in ('Queued', 'PausedReQueued') ORDER BY downloadStartTime, id LIMIT 20") - fun getQueuedDownloadsThatAreNotScheduledChunked(currentTime: Long) : Flow> + @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','PausedReQueued') ORDER BY downloadStartTime, id") + @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") fun getQueuedDownloadsList() : List - @Query("SELECT id FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id") + @Query("SELECT id FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY id") fun getQueuedDownloadsListIDs() : List @RewriteQueriesToDropUnusedColumns @@ -98,7 +98,6 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'") fun getPausedDownloadsList() : List - @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") fun getErroredDownloads() : PagingSource @@ -106,6 +105,10 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") fun getErroredDownloadsList() : List + + @Query("SELECT id from downloads WHERE status='Scheduled' ORDER BY downloadStartTime, id DESC") + fun getScheduledDownloadIDs(): List + @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC") fun getSavedDownloads() : PagingSource @@ -113,6 +116,10 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC") fun getSavedDownloadsList() : List + @RewriteQueriesToDropUnusedColumns + @Query("SELECT * FROM downloads WHERE status='Scheduled' ORDER BY downloadStartTime, id DESC") + fun getScheduledDownloads() : PagingSource + @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") fun getDownloadById(id: Long) : DownloadItem @@ -165,10 +172,13 @@ interface DownloadDao { @Query("DELETE FROM downloads WHERE status='Processing'") suspend fun deleteProcessing() + @Query("DELETE FROM downloads WHERE status='Scheduled'") + suspend fun deleteScheduled() + @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','PausedReQueued')") + @Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','QueuedPaused','Active','ActivePaused')") suspend fun cancelActiveQueued() @Query("DELETE FROM downloads WHERE status='Processing' AND id=:id") @@ -244,6 +254,22 @@ interface DownloadDao { } } + @Transaction + suspend fun putAtBottomOfTheQueue(existingIDs: List){ + val downloads = getQueuedDownloadsListIDs() + val newIDs = downloads.sortedByDescending { it }.take(existingIDs.size) + + resetScheduleTimeForItems(existingIDs) + existingIDs.forEach { updateDownloadID(it, -it) } + downloads.filter { !existingIDs.contains(it) }.reversed().forEach { + updateDownloadID(it, it + existingIDs.size) + } + + existingIDs.forEachIndexed { idx, it -> + updateDownloadID(-it, newIDs[idx]) + } + } + @Query("Update downloads set id=:newId where id=:id") suspend fun updateDownloadID(id: Long, newId: Long) @@ -251,6 +277,6 @@ interface DownloadDao { @Query("SELECT id from downloads WHERE id > :item1 AND id < :item2 AND status in (:statuses) ORDER BY id DESC") fun getIDsBetweenTwoItems(item1: Long, item2: Long, statuses: List) : List - @Query("SELECT id from downloads WHERE id > :item1 AND id < :item2 AND status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id") - fun getQueuedIDsBetweenTwoItems(item1: Long, item2: Long) : List + @Query("SELECT id from downloads WHERE id > :item1 AND id < :item2 AND status in('Scheduled') ORDER BY downloadStartTime, id") + fun getScheduledIDsBetweenTwoItems(item1: Long, item2: Long) : List } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/models/DownloadItemSimple.kt b/app/src/main/java/com/deniscerri/ytdl/database/models/DownloadItemSimple.kt index d7261770..bdd819dd 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/models/DownloadItemSimple.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/models/DownloadItemSimple.kt @@ -3,6 +3,7 @@ package com.deniscerri.ytdl.database.models import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel @Entity(tableName = "downloads") data class DownloadItemSimple( @@ -16,5 +17,8 @@ data class DownloadItemSimple( var format: Format, @ColumnInfo(defaultValue = "Queued") var status: String, - var logID: Long? + var logID: Long?, + var type: DownloadViewModel.Type, + @ColumnInfo(defaultValue = "0") + var downloadStartTime: Long, ) \ No newline at end of file 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 8df90b36..c5a3408c 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 @@ -53,15 +53,23 @@ class DownloadRepository(private val downloadDao: DownloadDao) { config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), pagingSourceFactory = {downloadDao.getSavedDownloads()} ) + val scheduledDownloads: Pager = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), + pagingSourceFactory = {downloadDao.getScheduledDownloads()} + ) - val activeDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.ActivePaused, Status.PausedReQueued).toListString()) + 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 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, PausedReQueued, Queued, QueuedPaused, Error, Cancelled, Saved, Processing + Active, ActivePaused, Queued, QueuedPaused, Error, Cancelled, Saved, Processing, Scheduled } suspend fun insert(item: DownloadItem) : Long { @@ -139,12 +147,20 @@ class DownloadRepository(private val downloadDao: DownloadDao) { return downloadDao.getErroredDownloadsList() } + fun getScheduledDownloadIDs() : List { + return downloadDao.getScheduledDownloadIDs() + } + suspend fun deleteCancelled(){ val cancelled = getCancelledDownloads() downloadDao.deleteCancelled() deleteCache(cancelled) } + suspend fun deleteScheduled() { + downloadDao.deleteScheduled() + } + suspend fun deleteErrored(){ val errored = getErroredDownloads() downloadDao.deleteErrored() 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 95f21d7b..c31f0a0c 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 @@ -35,6 +35,7 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.HistoryRepository import com.deniscerri.ytdl.database.repository.ResultRepository 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 @@ -65,12 +66,17 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val cancelledDownloads : Flow> val erroredDownloads : Flow> val savedDownloads : Flow> + 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 private var bestVideoFormat : Format private var bestAudioFormat : Format @@ -122,16 +128,21 @@ 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 activeDownloads = repository.activeDownloads processingDownloads = repository.processingDownloads savedDownloads = repository.savedDownloads.flow + scheduledDownloads = repository.scheduledDownloads.flow cancelledDownloads = repository.cancelledDownloads.flow erroredDownloads = repository.erroredDownloads.flow viewModelScope.launch(Dispatchers.IO){ @@ -652,6 +663,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel repository.deleteCancelled() } + fun deleteScheduled() = viewModelScope.launch(Dispatchers.IO) { + val scheduledIds = repository.getScheduledDownloadIDs() + scheduledIds.forEach { + WorkManager.getInstance(application).cancelAllWorkByTag(it.toString()) + } + repository.deleteScheduled() + } + fun deleteErrored() = viewModelScope.launch(Dispatchers.IO) { repository.deleteErrored() } @@ -679,11 +698,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel fun getCancelled() : List { return repository.getCancelledDownloads() } - - fun getPausedDownloads() : List { - return repository.getPausedDownloads() - } - fun getErrored() : List { return repository.getErroredDownloads() } @@ -714,9 +728,65 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel repository.startDownloadWorker(emptyList(), application) } + suspend fun startDownloadWorker(list: List){ + repository.startDownloadWorker(list, application) + } + suspend fun putAtTopOfQueue(ids: List) = CoroutineScope(Dispatchers.IO).launch{ - dbManager.downloadDao.putAtTopOfTheQueue(ids) - repository.startDownloadWorker(emptyList(), application) + val downloads = dao.getQueuedDownloadsListIDs() + val lastID = ids.maxOf { it } + ids.forEach { dao.updateDownloadID(it, -it) } + val newIDs = downloads.take(ids.size) + + //other ids that need to move around + val takenPositions = mutableListOf() + for (dID in downloads.filter { !ids.contains(it) && it < lastID }.reversed()){ + val newID = downloads.last { !newIDs.contains(it) && !takenPositions.contains(it) && it <= lastID } + takenPositions.add(newID) + dao.updateDownloadID(dID, newID) + } + + ids.forEachIndexed { idx, it -> + dao.updateDownloadID(-it, newIDs[idx]) + } + } + + + suspend fun putAtBottomOfQueue(ids: List) = CoroutineScope(Dispatchers.IO).launch{ + val downloads = dao.getQueuedDownloadsListIDs() + ids.forEach { dao.updateDownloadID(it, -it)} + val newIDs = downloads.sortedByDescending { it }.take(ids.size) + + //other ids that need to move around + val takenPositions = mutableListOf() + for (dID in downloads.filter { !ids.contains(it) }){ + val newID = downloads.first { !newIDs.contains(it) && !takenPositions.contains(it) } + takenPositions.add(newID) + dao.updateDownloadID(dID, newID) + } + + ids.reversed().forEachIndexed { idx, it -> + dao.updateDownloadID(-it, newIDs[idx]) + } + } + + + fun putAtPosition(current: Long, id: Long) = CoroutineScope(Dispatchers.IO).launch { + val downloads = dao.getQueuedDownloadsListIDs() + dao.updateDownloadID(current, -current) + + if (current > id){ + for (dID in downloads.filter { it in id until current }.reversed()){ + val index = downloads.indexOf(dID) + dao.updateDownloadID(dID, downloads[index + 1]) + } + }else{ + for (dID in downloads.filter { it in (current + 1)..id }){ + val index = downloads.indexOf(dID) + dao.updateDownloadID(dID, downloads[index - 1]) + } + } + dao.updateDownloadID(-current, id) } suspend fun reQueueDownloadItems(items: List) = CoroutineScope(Dispatchers.IO).launch { @@ -734,9 +804,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val existingDownloads = mutableListOf() val existingHistory = mutableListOf() - var ignoreDuplicate = ign - if (sharedPreferences.getBoolean("download_archive", false)) ignoreDuplicate = true - //if scheduler is on val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){ @@ -747,41 +814,94 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" } } + val downloadArchive = runCatching { File(FileUtil.getDownloadArchivePath(application)).useLines { it.toList() } }.getOrElse { listOf() } + .map { it.split(" ")[1] } items.forEach { - if (it.status != DownloadRepository.Status.ActivePaused.toString()) it.status = DownloadRepository.Status.Queued.toString() + if (! listOf(DownloadRepository.Status.ActivePaused, DownloadRepository.Status.Scheduled).toListString().contains(it.status)) + it.status = DownloadRepository.Status.Queued.toString() var alreadyExists = false - if(!ignoreDuplicate){ - val currentCommand = infoUtil.buildYoutubeDLRequest(it) - val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand) - val existingDownload = activeAndQueuedDownloads.firstOrNull{d -> - d.id = 0 - d.logID = null - d.customFileNameTemplate = it.customFileNameTemplate - d.status = DownloadRepository.Status.Queued.toString() - d.toString() == it.toString() - } - - if (existingDownload != null){ - it.status = DownloadRepository.Status.Saved.toString() - val id = runBlocking { - repository.insert(it) - } - alreadyExists = true - existingDownloads.add(id) - }else{ - //check if downloaded and file exists - val history = withContext(Dispatchers.IO){ - historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } } + val checkDuplicate = sharedPreferences.getString("prevent_duplicate_downloads", "")!! + if (checkDuplicate.isNotEmpty() && !ign){ + when(checkDuplicate){ + "download_archive" -> { + if (downloadArchive.any { d -> it.url.contains(d) }){ + alreadyExists = true + if (it.id == 0L) { + it.status = DownloadRepository.Status.Processing.toString() + val id = runBlocking { + repository.insert(it) + } + it.id = id + } + existingDownloads.add(it.id) + } } + "url_type" -> { + val existingDownload = activeAndQueuedDownloads.firstOrNull{d -> + d.id = 0 + d.logID = null + d.customFileNameTemplate = it.customFileNameTemplate + d.status = DownloadRepository.Status.Queued.toString() + d.toString() == it.toString() + } - val existingHistoryItem = history.firstOrNull { - h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") - } + if (existingDownload != null){ + it.status = DownloadRepository.Status.Processing.toString() + val id = runBlocking { + repository.insert(it) + } + alreadyExists = true + existingDownloads.add(id) + }else{ + //check if downloaded and file exists + val history = withContext(Dispatchers.IO){ + historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } } + } - if (existingHistoryItem != null){ - alreadyExists = true - existingHistory.add(existingHistoryItem.id) + val existingHistoryItem = history.firstOrNull { + h -> h.type == it.type + } + + if (existingHistoryItem != null){ + alreadyExists = true + existingHistory.add(existingHistoryItem.id) + } + } + } + "config" -> { + val currentCommand = infoUtil.buildYoutubeDLRequest(it) + val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand) + val existingDownload = activeAndQueuedDownloads.firstOrNull{d -> + d.id = 0 + d.logID = null + d.customFileNameTemplate = it.customFileNameTemplate + d.status = DownloadRepository.Status.Queued.toString() + d.toString() == it.toString() + } + + if (existingDownload != null){ + it.status = DownloadRepository.Status.Processing.toString() + val id = runBlocking { + repository.insert(it) + } + alreadyExists = true + existingDownloads.add(id) + }else{ + //check if downloaded and file exists + val history = withContext(Dispatchers.IO){ + historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } } + } + + val existingHistoryItem = history.firstOrNull { + h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") + } + + if (existingHistoryItem != null){ + alreadyExists = true + existingHistory.add(existingHistoryItem.id) + } + } } } } @@ -792,7 +912,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel repository.insert(it) } it.id = id - }else if (it.status == DownloadRepository.Status.Queued.toString()){ + }else if (listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.Scheduled).toListString().contains(it.status)){ withContext(Dispatchers.IO){ repository.update(it) } @@ -839,7 +959,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } fun getQueuedCollectedFileSize() : Long { - return dbManager.downloadDao.getSelectedFormatFromQueued().sumOf { it.filesize } + return dbManager.downloadDao.getSelectedFormatFromQueued().filter { it.filesize > 10 }.sumOf { it.filesize } } fun getTotalSize(status: List) : LiveData { @@ -862,6 +982,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel repository.getProcessingDownloads().apply { if (timeInMillis > 0){ this.forEach { + it.status = DownloadRepository.Status.Scheduled.toString() it.downloadStartTime = timeInMillis } } @@ -999,8 +1120,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel return dao.getIDsBetweenTwoItems(item1, item2, statuses) } - fun getQueuedIDsBetweenTwoItems(item1: Long, item2: Long) : List { - return dao.getQueuedIDsBetweenTwoItems(item1, item2) + fun getScheduledIDsBetweenTwoItems(item1: Long, item2: Long) : List { + return dao.getScheduledIDsBetweenTwoItems(item1, item2) } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ObserveSourcesViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ObserveSourcesViewModel.kt index 884c2bd3..1738ba14 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ObserveSourcesViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ObserveSourcesViewModel.kt @@ -11,6 +11,8 @@ import androidx.work.Constraints import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequest +import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem @@ -88,10 +90,6 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie Calendar.getInstance().apply { timeInMillis = it.startsTime - val date = Calendar.getInstance() - set(Calendar.DAY_OF_MONTH, date.get(Calendar.DAY_OF_MONTH)) - set(Calendar.MONTH, date.get(Calendar.MONTH)) - set(Calendar.YEAR, date.get(Calendar.YEAR)) if (it.everyCategory != ObserveSourcesRepository.EveryCategory.HOUR){ val hourMin = Calendar.getInstance() @@ -131,7 +129,7 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie .addTag("observeSources") .addTag(it.id.toString()) .setConstraints(workConstraints.build()) - .setInitialDelay(System.currentTimeMillis() - timeInMillis, TimeUnit.MILLISECONDS) + .setInitialDelay(timeInMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS) .setInputData(Data.Builder().putLong("id", it.id).build()) workManager.enqueueUniqueWork( 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 3c17ee81..65424e31 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt @@ -645,7 +645,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti fun scrollToTop() { recyclerView!!.scrollToPosition(0) - (searchBar!!.parent as AppBarLayout).setExpanded(true, true) + runCatching { (searchBar!!.parent as AppBarLayout).setExpanded(true, true) } } @SuppressLint("ResourceType") 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 c647f8c2..b796a065 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 @@ -116,11 +116,6 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: onItemClickListener.onOutputClick(item) } - - // PAUSE BUTTON ---------------------------------- - val pauseButton = card.findViewById(R.id.active_download_pause) - if (pauseButton.hasOnClickListeners()) pauseButton.setOnClickListener(null) - // CANCEL BUTTON ---------------------------------- val cancelButton = card.findViewById(R.id.active_download_delete) if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null) @@ -130,64 +125,18 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: when(DownloadRepository.Status.valueOf(item.status)){ DownloadRepository.Status.Active -> { progressBar.isIndeterminate = true - - val fromRadius: Int = dp(activity.resources, 30f) - val toRadius: Int = dp(activity.resources, 15f) - val animator = ValueAnimator.ofInt(fromRadius, toRadius) - animator.setDuration(500) - .addUpdateListener { animation -> - val value = animation.animatedValue as Int - pauseButton.cornerRadius = value - pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_pause_white) - pauseButton.contentDescription = activity.getString(R.string.pause) - pauseButton.isEnabled = true - pauseButton.tag = ActiveDownloadAction.Pause - } - animator.start() - - cancelButton.visibility = View.GONE + cancelButton.isEnabled = true } DownloadRepository.Status.ActivePaused -> { progressBar.isIndeterminate = false - - val fromRadius: Int = dp(activity.resources, 15f) - val toRadius: Int = dp(activity.resources, 30f) - val animator = ValueAnimator.ofInt(fromRadius, toRadius) - animator.setDuration(500) - .addUpdateListener { animation -> - val value = animation.animatedValue as Int - pauseButton.cornerRadius = value - pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_play_arrow_white) - pauseButton.contentDescription = activity.getString(R.string.resume) - pauseButton.tag = ActiveDownloadAction.Resume - pauseButton.isEnabled = true - } - animator.start() - cancelButton.visibility = View.VISIBLE - } - DownloadRepository.Status.PausedReQueued -> { - progressBar.isIndeterminate = true - pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.ic_refresh) - pauseButton.contentDescription = activity.getString(R.string.please_wait) - pauseButton.tag = null - pauseButton.isEnabled = false - cancelButton.visibility = View.GONE + cancelButton.isEnabled = true + output.text = activity.getString(R.string.exo_download_paused) } else -> {} } - - pauseButton.setOnClickListener { - if (pauseButton.tag == ActiveDownloadAction.Pause){ - onItemClickListener.onPauseClick(item.id, ActiveDownloadAction.Pause, position) - }else{ - onItemClickListener.onPauseClick(item.id, ActiveDownloadAction.Resume, position) - } - } - } interface OnItemClickListener { fun onCancelClick(itemID: Long) - fun onPauseClick(itemID: Long, action: ActiveDownloadAction, position: Int) 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 e270fb75..d585b7de 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 @@ -2,14 +2,19 @@ package com.deniscerri.ytdl.ui.adapter import android.app.Activity import android.content.SharedPreferences +import android.os.Build import android.os.Handler import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView +import android.widget.PopupMenu import android.widget.TextView import androidx.core.content.ContextCompat +import androidx.core.view.get +import androidx.core.view.isVisible +import androidx.media3.exoplayer.offline.Download import androidx.preference.PreferenceManager import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil @@ -18,6 +23,7 @@ import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.repository.DownloadRepository +import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.util.Extensions.loadThumbnail import com.deniscerri.ytdl.util.Extensions.popup import com.deniscerri.ytdl.util.FileUtil @@ -42,7 +48,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac val cardView: MaterialCardView init { - cardView = itemView.findViewById(R.id.active_download_card_view) + cardView = itemView.findViewById(R.id.download_card_view) } } @@ -58,7 +64,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac card.popup() val uiHandler = Handler(Looper.getMainLooper()) - val thumbnail = card.findViewById(R.id.image_view) + val thumbnail = card.findViewById(R.id.downloads_image_view) // THUMBNAIL ---------------------------------- val hideThumb = sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue") @@ -78,10 +84,26 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac } itemTitle.text = title.ifEmpty { item.url } + //DOWNLOAD TYPE ----------------------------- + val type = card.findViewById(R.id.download_type) + when(item.type){ + DownloadViewModel.Type.audio -> type.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_music_formatcard, 0, 0, 0 + ) + DownloadViewModel.Type.video -> type.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_video_formatcard, 0, 0, 0 + ) + else -> type.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_terminal_formatcard, 0, 0, 0 + ) + } - val formatDetailsChip = card.findViewById(R.id.format_note) - val formatDetailsText = StringBuilder(item.format.format_note.uppercase()) + val formatNote = card.findViewById(R.id.format_note) + if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility = + View.GONE + else formatNote!!.text = item.format.format_note.uppercase() + val codec = card.findViewById(R.id.codec) val codecText = if (item.format.encoding != "") { item.format.encoding.uppercase() @@ -90,56 +112,61 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac } else { item.format.acodec.uppercase() } - if (codecText != "" && codecText != "none"){ - formatDetailsText.append(" \t •\t $codecText") - } - - val fileSize = FileUtil.convertFileSize(item.format.filesize) - if (fileSize != "?") formatDetailsText.append(" \t •\t $fileSize") - - formatDetailsChip.text = formatDetailsText - - // PAUSE BUTTON ---------------------------------- - val pauseButton = card.findViewById(R.id.active_download_pause) - if (pauseButton.hasOnClickListeners()) pauseButton.setOnClickListener(null) - - // CANCEL BUTTON ---------------------------------- - val cancelButton = card.findViewById(R.id.active_download_delete) - if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null) - cancelButton.setOnClickListener {onItemClickListener.onCancelClick(item.id)} - - if (item.status == DownloadRepository.Status.ActivePaused.toString()){ - progressBar.isIndeterminate = false - pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_play_arrow_white) - pauseButton.contentDescription = activity.getString(R.string.start) - pauseButton.tag = ActiveDownloadAdapter.ActiveDownloadAction.Resume - cancelButton.visibility = View.VISIBLE + if (codecText == "" || codecText == "none"){ + codec.visibility = View.GONE }else{ - progressBar.isIndeterminate = true - pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_pause_white) - pauseButton.contentDescription = activity.getString(R.string.pause) - cancelButton.visibility = View.GONE - pauseButton.tag = ActiveDownloadAdapter.ActiveDownloadAction.Pause + codec.visibility = View.VISIBLE + codec.text = codecText } - pauseButton.setOnClickListener { - if (pauseButton.tag == ActiveDownloadAdapter.ActiveDownloadAction.Pause){ - onItemClickListener.onPauseClick(item.id, - ActiveDownloadAdapter.ActiveDownloadAction.Pause, position) - pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_play_arrow_white) - if (progressBar.progress == 0) progressBar.isIndeterminate = false - cancelButton.visibility = View.VISIBLE - pauseButton.tag = ActiveDownloadAdapter.ActiveDownloadAction.Resume + val fileSize = card.findViewById(R.id.file_size) + val fileSizeReadable = FileUtil.convertFileSize(item.format.filesize) + if (fileSizeReadable == "?") fileSize.visibility = View.GONE + else fileSize.text = fileSizeReadable + + val menu = card.findViewById(R.id.options) + menu.setOnClickListener { + val popup = PopupMenu(activity, it) + popup.menuInflater.inflate(R.menu.active_downloads_minified, popup.menu) + if (Build.VERSION.SDK_INT > 27) popup.menu.setGroupDividerEnabled(true) + + val pause = popup.menu[0] + val resume = popup.menu[1] + + if (item.status == DownloadRepository.Status.ActivePaused.toString()){ + pause.isVisible = false + resume.isVisible = true }else{ - onItemClickListener.onPauseClick(item.id, - ActiveDownloadAdapter.ActiveDownloadAction.Resume, position) - pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_pause_white) - progressBar.isIndeterminate = true - cancelButton.visibility = View.GONE - pauseButton.tag = ActiveDownloadAdapter.ActiveDownloadAction.Pause + pause.isVisible = true + resume.isVisible = false } + + popup.setOnMenuItemClickListener { m -> + when(m.itemId){ + R.id.pause -> { + onItemClickListener.onPauseClick(item.id, ActiveDownloadAdapter.ActiveDownloadAction.Pause, position) + if (progressBar.progress == 0) progressBar.isIndeterminate = false + popup.dismiss() + } + R.id.resume -> { + onItemClickListener.onPauseClick(item.id, ActiveDownloadAdapter.ActiveDownloadAction.Resume, position) + progressBar.isIndeterminate = true + popup.dismiss() + } + R.id.cancel -> { + onItemClickListener.onCancelClick(item.id) + popup.dismiss() + } + } + true + } + + popup.show() + } + progressBar.isIndeterminate = item.status != DownloadRepository.Status.ActivePaused.toString() + card.setOnClickListener { onItemClickListener.onCardClick() } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ConfigureMultipleDownloadsAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ConfigureMultipleDownloadsAdapter.kt index b7fba551..2365b834 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ConfigureMultipleDownloadsAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ConfigureMultipleDownloadsAdapter.kt @@ -10,6 +10,7 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import androidx.preference.PreferenceManager import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil @@ -76,6 +77,8 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener } itemTitle.text = title + card.findViewById(R.id.download_type).isVisible = false + // Format Note ---------------------------------- val formatNote = card.findViewById(R.id.format_note) if (item.format.format_note.isNotEmpty()){ diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/GenericDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/GenericDownloadAdapter.kt index c3121be1..5594b0cd 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/GenericDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/GenericDownloadAdapter.kt @@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.DownloadItemSimple import com.deniscerri.ytdl.database.repository.DownloadRepository +import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.util.Extensions.loadThumbnail import com.deniscerri.ytdl.util.Extensions.popup import com.deniscerri.ytdl.util.FileUtil @@ -79,6 +80,20 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: } itemTitle.text = title.ifEmpty { item.url } + //DOWNLOAD TYPE ----------------------------- + val type = card.findViewById(R.id.download_type) + when(item.type){ + DownloadViewModel.Type.audio -> type.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_music_formatcard, 0,0,0 + ) + DownloadViewModel.Type.video -> type.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_video_formatcard, 0,0,0 + ) + else -> type.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_terminal_formatcard, 0,0,0 + ) + } + val formatNote = card.findViewById(R.id.format_note) if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility = View.GONE diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HistoryAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HistoryAdapter.kt index 959e0e98..b73a8e26 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HistoryAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HistoryAdapter.kt @@ -127,6 +127,7 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit btn.backgroundTintList = MaterialColors.getColorStateList(activity, R.attr.colorSurface, ContextCompat.getColorStateList(activity, android.R.color.transparent)!!) }else{ thumbnail.alpha = 1f + thumbnail.colorFilter = null btn.backgroundTintList = MaterialColors.getColorStateList(activity, R.attr.colorPrimaryContainer, ContextCompat.getColorStateList(activity, android.R.color.transparent)!!) } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ObserveSourcesAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ObserveSourcesAdapter.kt index 64c823f8..6331d24c 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ObserveSourcesAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ObserveSourcesAdapter.kt @@ -15,7 +15,7 @@ import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository -import com.deniscerri.ytdl.util.Extensions.calculateNextTime +import com.deniscerri.ytdl.util.Extensions.calculateNextTimeForObserving import com.deniscerri.ytdl.util.Extensions.popup import com.google.android.material.card.MaterialCardView import com.google.android.material.chip.Chip @@ -76,7 +76,7 @@ class ObserveSourcesAdapter(onItemClickListener: OnItemClickListener, activity: //INFO val info = card.findViewById(R.id.info) - val nextTime = item.calculateNextTime() + val nextTime = item.calculateNextTimeForObserving() val c = Calendar.getInstance() c.timeInMillis = nextTime @@ -149,7 +149,7 @@ class ObserveSourcesAdapter(onItemClickListener: OnItemClickListener, activity: && oldItem.status == newItem.status && oldItem.retryMissingDownloads == newItem.retryMissingDownloads && oldItem.runCount == newItem.runCount - && oldItem.everyCategory == newItem.everyCategory + && oldItem.calculateNextTimeForObserving() == newItem.calculateNextTimeForObserving() } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/QueuedDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/QueuedDownloadAdapter.kt new file mode 100644 index 00000000..d7c725d7 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/QueuedDownloadAdapter.kt @@ -0,0 +1,272 @@ +package com.deniscerri.ytdl.ui.adapter + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.SharedPreferences +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.PopupMenu +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.paging.PagingDataAdapter +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.database.models.DownloadItemSimple +import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel +import com.deniscerri.ytdl.util.Extensions.loadThumbnail +import com.deniscerri.ytdl.util.Extensions.popup +import com.deniscerri.ytdl.util.FileUtil +import com.deniscerri.ytdl.util.UiUtil +import com.google.android.material.card.MaterialCardView + +class QueuedDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity, private var itemTouchHelper: ItemTouchHelper) : PagingDataAdapter( + DIFF_CALLBACK +) { + private val onItemClickListener: OnItemClickListener + private val activity: Activity + val checkedItems: MutableSet = mutableSetOf() + var inverted: Boolean + var showDragHandle: Boolean + private val sharedPreferences: SharedPreferences + + init { + this.onItemClickListener = onItemClickListener + this.activity = activity + this.inverted = false + this.showDragHandle = false + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) + } + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val cardView: MaterialCardView + init { + cardView = itemView.findViewById(R.id.download_card_view) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val cardView = LayoutInflater.from(parent.context) + .inflate(R.layout.queued_download_card, parent, false) + return ViewHolder(cardView) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + val card = holder.cardView + + if (item == null) return + card.tag = item.id.toString() + + val uiHandler = Handler(Looper.getMainLooper()) + val thumbnail = card.findViewById(R.id.downloads_image_view) + + //DRAG HANDLE + val dragView = card.findViewById(R.id.drag_view) + dragView.isVisible = showDragHandle + card.findViewById(R.id.drag_view).setOnTouchListener { view, motionEvent -> + view.performClick() + if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN){ + itemTouchHelper.startDrag(holder) + } + true + } + + // THUMBNAIL ---------------------------------- + val hideThumb = sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue") + uiHandler.post { thumbnail.loadThumbnail(hideThumb, item.thumb) } + + val duration = card.findViewById(R.id.duration) + duration.text = item.duration + + // TITLE ---------------------------------- + val itemTitle = card.findViewById(R.id.title) + var title = item.title + if (title.length > 100) { + title = title.substring(0, 40) + "..." + } + itemTitle.text = title.ifEmpty { item.url } + + //DOWNLOAD TYPE ----------------------------- + val type = card.findViewById(R.id.download_type) + when(item.type){ + DownloadViewModel.Type.audio -> type.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_music_formatcard, 0,0,0 + ) + DownloadViewModel.Type.video -> type.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_video_formatcard, 0,0,0 + ) + else -> type.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_terminal_formatcard, 0,0,0 + ) + } + + val formatNote = card.findViewById(R.id.format_note) + if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility = + View.GONE + else formatNote!!.text = item.format.format_note.uppercase() + + val codec = card.findViewById(R.id.codec) + val codecText = + if (item.format.encoding != "") { + item.format.encoding.uppercase() + }else if (item.format.vcodec != "none" && item.format.vcodec != ""){ + item.format.vcodec.uppercase() + } else { + item.format.acodec.uppercase() + } + if (codecText == "" || codecText == "none"){ + codec.visibility = View.GONE + }else{ + codec.visibility = View.VISIBLE + codec.text = codecText + } + + val fileSize = card.findViewById(R.id.file_size) + val fileSizeReadable = FileUtil.convertFileSize(item.format.filesize) + if (fileSizeReadable == "?") fileSize.visibility = View.GONE + else fileSize.text = fileSizeReadable + + val menu = card.findViewById(R.id.options) + menu.setOnClickListener { + val popup = PopupMenu(activity, it) + popup.menuInflater.inflate(R.menu.queued_download_menu, popup.menu) + if (Build.VERSION.SDK_INT > 27) popup.menu.setGroupDividerEnabled(true) + + popup.setOnMenuItemClickListener { m -> + when(m.itemId){ + R.id.cancel -> { + onItemClickListener.onQueuedCancelClick(item.id) + popup.dismiss() + } + R.id.move_top -> { + onItemClickListener.onMoveQueuedItemToTop(item.id) + popup.dismiss() + } + R.id.move_bottom -> { + onItemClickListener.onMoveQueuedItemToBottom(item.id) + popup.dismiss() + } + R.id.copy_url -> { + UiUtil.copyLinkToClipBoard(activity, item.url) + popup.dismiss() + } + } + true + } + + popup.show() + + } + + if ((checkedItems.contains(item.id) && !inverted) || (!checkedItems.contains(item.id) && inverted)) { + card.isChecked = true + card.strokeWidth = 5 + } else { + card.isChecked = false + card.strokeWidth = 0 + } + card.findViewById(R.id.card_content).setOnClickListener { + if (checkedItems.size > 0 || inverted) { + checkCard(card, item.id, position) + } else { + onItemClickListener.onQueuedCardClick(item.id) + } + } + + card.findViewById(R.id.card_content).setOnLongClickListener { + checkCard(card, item.id, position) + true + } + + } + + @SuppressLint("NotifyDataSetChanged") + fun clearCheckedItems() { + inverted = false + checkedItems.clear() + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun checkAll() { + checkedItems.clear() + inverted = true + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun checkMultipleItems(list: List){ + checkedItems.clear() + inverted = false + checkedItems.addAll(list) + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun invertSelected() { + inverted = !inverted + notifyDataSetChanged() + } + + fun getSelectedObjectsCount(totalSize: Int) : Int{ + return if (inverted){ + totalSize - checkedItems.size + }else{ + checkedItems.size + } + } + + @SuppressLint("NotifyDataSetChanged") + fun toggleShowDragHandle(){ + showDragHandle = !showDragHandle + notifyDataSetChanged() + } + + + + private fun checkCard(card: MaterialCardView, itemID: Long, position: Int) { + if (card.isChecked) { + card.strokeWidth = 0 + if (inverted) checkedItems.add(itemID) + else checkedItems.remove(itemID) + } else { + card.strokeWidth = 5 + if (inverted) checkedItems.remove(itemID) + else checkedItems.add(itemID) + } + + card.isChecked = !card.isChecked + onItemClickListener.onQueuedCardSelect(card.isChecked, position) + } + + interface OnItemClickListener { + fun onQueuedCancelClick(itemID: Long) + fun onMoveQueuedItemToTop(itemID: Long) + fun onMoveQueuedItemToBottom(itemID: Long) + fun onQueuedCardClick(itemID: Long) + fun onQueuedCardSelect(isChecked: Boolean, position: Int) + } + + companion object { + private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean { + val ranged = arrayListOf(oldItem.id, newItem.id) + return ranged[0] == ranged[1] + } + + override fun areContentsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean { + return oldItem.id == newItem.id && oldItem.title == newItem.title && oldItem.author == newItem.author && oldItem.thumb == newItem.thumb + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ScheduledDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ScheduledDownloadAdapter.kt new file mode 100644 index 00000000..b5034afd --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ScheduledDownloadAdapter.kt @@ -0,0 +1,237 @@ +package com.deniscerri.ytdl.ui.adapter + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.SharedPreferences +import android.os.Handler +import android.os.Looper +import android.text.format.DateFormat +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.paging.PagingDataAdapter +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.database.models.DownloadItemSimple +import com.deniscerri.ytdl.database.repository.DownloadRepository +import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel +import com.deniscerri.ytdl.util.Extensions.loadThumbnail +import com.deniscerri.ytdl.util.Extensions.popup +import com.deniscerri.ytdl.util.FileUtil +import com.google.android.material.button.MaterialButton +import com.google.android.material.card.MaterialCardView +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +class ScheduledDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : PagingDataAdapter( + DIFF_CALLBACK +) { + private val onItemClickListener: OnItemClickListener + private val activity: Activity + val checkedItems: MutableSet = mutableSetOf() + var inverted: Boolean + private val sharedPreferences: SharedPreferences + + init { + this.onItemClickListener = onItemClickListener + this.activity = activity + this.inverted = false + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) + } + + class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { + val mainView: LinearLayout + init { + mainView = itemView.findViewById(R.id.scheduled_download_card_view) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val cardView = LayoutInflater.from(parent.context) + .inflate(R.layout.scheduled_download_card, parent, false) + return ViewHolder(cardView, onItemClickListener) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + val mainView = holder.mainView + val card = mainView.findViewById(R.id.download_card_view) + card.popup() + + if (item == null) return + card.tag = item.id.toString() + + //Scheduled Time + val time = mainView.findViewById(R.id.scheduled_time) + val calendar = Calendar.getInstance() + calendar.timeInMillis = item.downloadStartTime + time!!.text = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(calendar.time) + + val uiHandler = Handler(Looper.getMainLooper()) + val thumbnail = card.findViewById(R.id.downloads_image_view) + + // THUMBNAIL ---------------------------------- + val hideThumb = sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue") + uiHandler.post { thumbnail.loadThumbnail(hideThumb, item.thumb) } + + val duration = card.findViewById(R.id.duration) + duration.text = item.duration + + // TITLE ---------------------------------- + val itemTitle = card.findViewById(R.id.title) + var title = item.title + if (title.length > 100) { + title = title.substring(0, 40) + "..." + } + itemTitle.text = title.ifEmpty { item.url } + + //DOWNLOAD TYPE ----------------------------- + val type = card.findViewById(R.id.download_type) + when(item.type){ + DownloadViewModel.Type.audio -> type.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_music_formatcard, 0,0,0 + ) + DownloadViewModel.Type.video -> type.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_video_formatcard, 0,0,0 + ) + else -> type.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_terminal_formatcard, 0,0,0 + ) + } + + val formatNote = card.findViewById(R.id.format_note) + if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility = + View.GONE + else formatNote!!.text = item.format.format_note.uppercase() + + val codec = card.findViewById(R.id.codec) + val codecText = + if (item.format.encoding != "") { + item.format.encoding.uppercase() + }else if (item.format.vcodec != "none" && item.format.vcodec != ""){ + item.format.vcodec.uppercase() + } else { + item.format.acodec.uppercase() + } + if (codecText == "" || codecText == "none"){ + codec.visibility = View.GONE + }else{ + codec.visibility = View.VISIBLE + codec.text = codecText + } + + val fileSize = card.findViewById(R.id.file_size) + val fileSizeReadable = FileUtil.convertFileSize(item.format.filesize) + if (fileSizeReadable == "?") fileSize.visibility = View.GONE + else fileSize.text = fileSizeReadable + + // ACTION BUTTON ---------------------------------- + val actionButton = card.findViewById(R.id.action_button) + if (actionButton.hasOnClickListeners()) actionButton.setOnClickListener(null) + + actionButton.setIconResource(R.drawable.ic_downloads) + actionButton.contentDescription = activity.getString(R.string.download) + + actionButton.setOnClickListener { + onItemClickListener.onActionButtonClick(item.id) + } + if ((checkedItems.contains(item.id) && !inverted) || (!checkedItems.contains(item.id) && inverted)) { + card.isChecked = true + card.strokeWidth = 5 + } else { + card.isChecked = false + card.strokeWidth = 0 + } + card.setOnClickListener { + if (checkedItems.size > 0 || inverted) { + checkCard(card, item.id, position) + } else { + onItemClickListener.onCardClick(item.id) + } + } + + card.setOnLongClickListener { + checkCard(card, item.id, position) + true + } + + } + + @SuppressLint("NotifyDataSetChanged") + fun clearCheckedItems() { + inverted = false + checkedItems.clear() + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun checkAll() { + checkedItems.clear() + inverted = true + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun checkMultipleItems(list: List){ + checkedItems.clear() + inverted = false + checkedItems.addAll(list) + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun invertSelected() { + inverted = !inverted + notifyDataSetChanged() + } + + fun getSelectedObjectsCount(totalSize: Int) : Int{ + return if (inverted){ + totalSize - checkedItems.size + }else{ + checkedItems.size + } + } + + + + private fun checkCard(card: MaterialCardView, itemID: Long, position: Int) { + if (card.isChecked) { + card.strokeWidth = 0 + if (inverted) checkedItems.add(itemID) + else checkedItems.remove(itemID) + } else { + card.strokeWidth = 5 + if (inverted) checkedItems.remove(itemID) + else checkedItems.add(itemID) + } + + card.isChecked = !card.isChecked + onItemClickListener.onCardSelect(card.isChecked, position) + } + + interface OnItemClickListener { + fun onActionButtonClick(itemID: Long) + fun onCardClick(itemID: Long) + fun onCardSelect(isChecked: Boolean, position: Int) + } + + companion object { + private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean { + val ranged = arrayListOf(oldItem.id, newItem.id) + return ranged[0] == ranged[1] + } + + override fun areContentsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean { + return oldItem.id == newItem.id && oldItem.title == newItem.title && oldItem.author == newItem.author && oldItem.thumb == newItem.thumb + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/AddExtraCommandsDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/AddExtraCommandsDialog.kt index f5919dee..6877d5e1 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/AddExtraCommandsDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/AddExtraCommandsDialog.kt @@ -15,6 +15,7 @@ import android.view.inputmethod.InputMethodManager import android.widget.* import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.DownloadItem @@ -24,12 +25,13 @@ import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.UiUtil import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.elevation.SurfaceColors import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class AddExtraCommandsDialog(private val item: DownloadItem?, private val callback: ExtraCommandsListener) : BottomSheetDialogFragment() { +class AddExtraCommandsDialog(private val item: DownloadItem? = null, private val callback: ExtraCommandsListener? = null) : BottomSheetDialogFragment() { private lateinit var infoUtil: InfoUtil private lateinit var sharedPreferences: SharedPreferences private lateinit var commandTemplateViewModel: CommandTemplateViewModel @@ -64,14 +66,17 @@ class AddExtraCommandsDialog(private val item: DownloadItem?, private val callba WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) val view = LayoutInflater.from(context).inflate(R.layout.result_card_details, null) dialog.setContentView(view) - - + dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity()) } @SuppressLint("SetTextI18n") @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + if (callback == null){ + this.dismiss() + return + } val behavior = BottomSheetBehavior.from(view.parent as View) behavior.state = BottomSheetBehavior.STATE_EXPANDED diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt index bab689c8..876caa38 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt @@ -29,6 +29,7 @@ import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.util.UiUtil import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.elevation.SurfaceColors import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout import kotlinx.coroutines.Dispatchers @@ -66,6 +67,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private super.setupDialog(dialog, style) val view = LayoutInflater.from(context).inflate(R.layout.configure_download_bottom_sheet, null) dialog.setContentView(view) + dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity()) dialog.setOnShowListener { behavior = BottomSheetBehavior.from(view.parent as View) val displayMetrics = DisplayMetrics() 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 1b8119a3..01952a08 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 @@ -41,6 +41,7 @@ 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.elevation.SurfaceColors import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.slider.RangeSlider import com.google.android.material.textfield.TextInputLayout @@ -57,7 +58,7 @@ import java.util.* import kotlin.properties.Delegates -class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls : String?, private var chapters: List?, private val listener: VideoCutListener) : BottomSheetDialogFragment() { +class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private val urls : String? = null, private var chapters: List? = null, private val listener: VideoCutListener? = null) : BottomSheetDialogFragment() { private lateinit var behavior: BottomSheetBehavior private lateinit var infoUtil: InfoUtil private lateinit var player: ExoPlayer @@ -82,6 +83,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls private lateinit var resetBtn : Button private lateinit var chipGroup : ChipGroup private lateinit var suggestedLabel : View + private lateinit var item: DownloadItem private var timeSeconds by Delegates.notNull() private lateinit var selectedCuts: MutableList @@ -98,6 +100,14 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls super.setupDialog(dialog, style) val view = LayoutInflater.from(context).inflate(R.layout.cut_video_sheet, null) dialog.setContentView(view) + dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity()) + + if (_item == null){ + this.dismiss() + return + } + + item = _item dialog.setOnShowListener { behavior = BottomSheetBehavior.from(view.parent as View) @@ -251,9 +261,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls rewindBtn.setOnClickListener { try { val seconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString()) - val endTimestamp = (rangeSlider.valueTo.toInt() * timeSeconds) / 100 - val startValue = (seconds.toFloat() / endTimestamp) * 100 - player.seekTo((((startValue * timeSeconds) / 100) * 1000).toLong()) + player.seekTo((seconds * 1000).toLong()) player.play() }catch (ignored: Exception) {} } @@ -269,12 +277,15 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls } - @SuppressLint("SetTextI18n") + @SuppressLint("SetTextI18n", "ClickableViewAccessibility") private fun initCutSection(){ fromTextInput.editText!!.setTextAndRecalculateWidth("0:00") toTextInput.editText!!.setTextAndRecalculateWidth(item.duration) - rangeSlider.setOnTouchListener { v, event -> // Handle touch events here + rangeSlider.valueFrom = 0f + rangeSlider.valueTo = timeSeconds.toFloat() + rangeSlider.setValues(0F, timeSeconds.toFloat()) + rangeSlider.setOnTouchListener { _, event -> // Handle touch events here when (event.action) { MotionEvent.ACTION_MOVE -> { updateFromSlider() @@ -287,11 +298,8 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls // Return 'false' to allow the event to continue propagating or 'true' to consume it false } - rangeSlider.performClick() + rangeSlider.addOnChangeListener { rangeSlider, fl, b -> - rangeSlider.setOnDragListener { view, dragEvent -> - updateFromSlider() - false } fromTextInput.editText!!.setOnKeyListener(object : View.OnKeyListener { @@ -300,29 +308,25 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls if ((event!!.action == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { - var startTimestamp = (rangeSlider.valueFrom.toInt() * timeSeconds) / 100 - val endTimestamp = (rangeSlider.valueTo.toInt() * timeSeconds) / 100 + var startTimestamp = rangeSlider.valueFrom.toInt() + val endTimestamp = rangeSlider.valueTo.toInt() fromTextInput.editText!!.clearFocus() var seconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString()) val endSeconds = convertStringToTimestamp(toTextInput.editText!!.text.toString()) - var startValue = (seconds.toFloat() / endTimestamp) * 100 - val endValue = (endSeconds.toFloat() / endTimestamp) * 100 - if (seconds == 0) { fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US)) - }else if (startValue > 100){ + }else if (seconds > endSeconds){ startTimestamp = 0 seconds = 0 fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US)) - startValue = 0F } fromTextInput.editText!!.setTextAndRecalculateWidth(fromTextInput.editText!!.text.toString()) - rangeSlider.setValues(startValue, endValue) - okBtn.isEnabled = startValue != 0F || endValue != 100F + rangeSlider.setValues(seconds.toFloat(), endSeconds.toFloat()) + okBtn.isEnabled = seconds != 0 || endSeconds != timeSeconds try { player.seekTo(seconds.toLong() * 1000) player.play() @@ -345,28 +349,26 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls toTextInput.editText!!.clearFocus() val startSeconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString()) - val seconds = convertStringToTimestamp(toTextInput.editText!!.text.toString()) + var endSeconds = convertStringToTimestamp(toTextInput.editText!!.text.toString()) - val startValue = (startSeconds.toFloat() / endTimestamp) * 100 - var endValue = (seconds.toFloat() / endTimestamp) * 100 - if (endValue > 100F){ + if (endSeconds > timeSeconds){ endTimestamp = timeSeconds toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US)) - endValue = 100F + endSeconds = timeSeconds } - if (seconds == 0) { + if (endSeconds == 0) { toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US)) - }else if (endValue <= rangeSlider.valueFrom.toInt()){ + }else if (endSeconds <= rangeSlider.valueFrom.toInt()){ toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US)) } toTextInput.editText!!.setTextAndRecalculateWidth(toTextInput.editText!!.text.toString()) - rangeSlider.setValues(startValue, endValue) - okBtn.isEnabled = startValue != 0F || endValue != 100F + rangeSlider.setValues(startSeconds.toFloat(), endSeconds.toFloat()) + okBtn.isEnabled = startSeconds != 0 || endSeconds != timeSeconds try { - player.seekTo((seconds.toLong() - 4L) * 1000) + player.seekTo((endSeconds.toLong() - 2L) * 1000) player.play() }catch (ignored: Exception) {} @@ -400,20 +402,32 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls private fun updateFromSlider(){ val values = rangeSlider.values - val startTimestamp = (values[0].toInt() * timeSeconds) / 100 - val endTimestamp = (values[1].toInt() * timeSeconds) / 100 + val startTimestamp = values[0].toInt() + val endTimestamp = values[1].toInt() val startTimestampString = startTimestamp.toStringDuration(Locale.US) val endTimestampString = endTimestamp.toStringDuration(Locale.US) + var draggedFromBeginning = true + if (toTextInput.editText!!.text.toString() != endTimestampString){ + draggedFromBeginning = false + } + fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestampString) toTextInput.editText!!.setTextAndRecalculateWidth(endTimestampString) - okBtn.isEnabled = values[0] != 0F || values[1] != 100F + okBtn.isEnabled = values[0] != 0F || values[1] != timeSeconds.toFloat() + val startpos = (startTimestamp * 1000).toLong() try { - player.seekTo((startTimestamp * 1000).toLong()) + if (draggedFromBeginning){ + player.seekTo(startpos) + }else{ + var endpos = (endTimestamp * 1000).toLong() - 1500 + if (endpos < (startTimestamp * 1000).toLong()) endpos = startpos + player.seekTo(endpos) + } player.play() }catch (ignored: Exception) {} } @@ -452,7 +466,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls newCutBtn.setOnClickListener { cutSection.visibility = View.VISIBLE cutListSection.visibility = View.GONE - rangeSlider.setValues(0F, 100F) + rangeSlider.setValues(0F, timeSeconds.toFloat()) player.seekTo(0) suggestedChips.children.apply { this.forEach { @@ -464,7 +478,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls resetBtn.setOnClickListener { chipGroup.removeAllViews() - listener.onChangeCut(emptyList()) + listener?.onChangeCut(emptyList()) player.stop() dismiss() } @@ -484,20 +498,17 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls val startTimestamp = convertStringToTimestamp(timestamp.split("-")[0].replace(";", "")) val endTimestamp = convertStringToTimestamp(timestamp.split("-")[1].replace(";", "")) - val startingValue = ((startTimestamp.toFloat() / timeSeconds) * 100).toInt() - val endingValue = ((endTimestamp.toFloat() / timeSeconds) * 100).toInt() - val chip = layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip chip.text = timestamp chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK)) chip.isCheckedIconVisible = false chipGroup.addView(chip) selectedCuts.add(chip.text.toString()) - listener.onChangeCut(selectedCuts) + listener?.onChangeCut(selectedCuts) chip.setOnClickListener { if (chip.isChecked) { - rangeSlider.setValues(startingValue.toFloat(), endingValue.toFloat()) + rangeSlider.setValues(startTimestamp.toFloat(), endTimestamp.toFloat()) player.prepare() player.seekTo((startTimestamp * 1000).toLong()) player.play() @@ -513,7 +524,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls player.pause() chipGroup.removeView(chip) selectedCuts.remove(chip.text.toString()) - listener.onChangeCut(selectedCuts) + listener?.onChangeCut(selectedCuts) if (selectedCuts.isEmpty()){ player.stop() dismiss() @@ -539,21 +550,18 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls if (! selectedCuts.contains(timestamp)) selectedCuts.add(timestamp) - listener.onChangeCut(selectedCuts) + listener?.onChangeCut(selectedCuts) if (chapter.start_time == 0L && chapter.end_time == 0L) { chip.isEnabled = false }else{ val startTimestamp = chapter.start_time.toInt() val endTimestamp = chapter.end_time.toInt() - val startingValue = ((startTimestamp.toFloat() / timeSeconds) * 100).toInt() - val endingValue = ((endTimestamp.toFloat() / timeSeconds) * 100).toInt() - chip.setOnClickListener { if (chip.isChecked) { - rangeSlider.setValues(startingValue.toFloat(), endingValue.toFloat()) + rangeSlider.setValues(startTimestamp.toFloat(), endTimestamp.toFloat()) player.prepare() - player.seekTo((((startingValue * timeSeconds) / 100) * 1000).toLong()) + player.seekTo(((startTimestamp) * 1000).toLong()) player.play() }else { player.seekTo(0) @@ -570,7 +578,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls player.pause() chipGroup.removeView(chip) selectedCuts.remove(chip.text.toString()) - listener.onChangeCut(selectedCuts) + listener?.onChangeCut(selectedCuts) if (selectedCuts.isEmpty()){ player.stop() dismiss() 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 bc4ba75b..a6d139c9 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 @@ -46,6 +46,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.elevation.SurfaceColors import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout @@ -127,6 +128,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { super.setupDialog(dialog, style) view = LayoutInflater.from(context).inflate(R.layout.download_bottom_sheet, null) dialog.setContentView(view) + dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity()) dialog.setOnShowListener { behavior = BottomSheetBehavior.from(view.parent as View) @@ -313,6 +315,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { scheduleBtn.isEnabled = false download.isEnabled = false val item: DownloadItem = getDownloadItem() + item.status = DownloadRepository.Status.Scheduled.toString() item.downloadStartTime = it.timeInMillis if (item.videoPreferences.alsoDownloadAsAudio){ val itemsToQueue = mutableListOf() diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadCommandFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadCommandFragment.kt index f910c82d..f067cae8 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadCommandFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadCommandFragment.kt @@ -80,8 +80,6 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat if (type != DownloadViewModel.Type.command){ type = DownloadViewModel.Type.command } - - format = downloadViewModel.getFormat(allFormats, DownloadViewModel.Type.command) } val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java) 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 ea9ec9bf..6a975c1d 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 @@ -48,6 +48,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment 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.snackbar.Snackbar import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import kotlinx.coroutines.CoroutineScope @@ -92,6 +93,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure super.setupDialog(dialog, style) val view = LayoutInflater.from(context).inflate(R.layout.download_multiple_bottom_sheet, null) dialog.setContentView(view) + dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity()) dialog.setOnShowListener { behavior = BottomSheetBehavior.from(view.parent as View) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt index aa152611..5ec584e2 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt @@ -36,7 +36,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class FormatSelectionBottomSheetDialog(private val items: List, private var formats: List>, private val listener: OnFormatClickListener) : BottomSheetDialogFragment() { +class FormatSelectionBottomSheetDialog(private val _items: List? = null, private var _formats: List>? = null, private val _listener: OnFormatClickListener? = null) : BottomSheetDialogFragment() { private lateinit var behavior: BottomSheetBehavior private lateinit var infoUtil: InfoUtil private lateinit var downloadViewModel: DownloadViewModel @@ -62,6 +62,10 @@ class FormatSelectionBottomSheetDialog(private val items: List, p private var hasGenericFormats: Boolean = false + private lateinit var items: List + private lateinit var formats: List> + private lateinit var listener: OnFormatClickListener + enum class FormatSorting { filesize, container, codec, id } @@ -87,6 +91,15 @@ class FormatSelectionBottomSheetDialog(private val items: List, p view = LayoutInflater.from(context).inflate(R.layout.format_select_bottom_sheet, null) dialog.setContentView(view) + if (_items == null){ + this.dismiss() + return + } + + items = _items + formats = _formats!! + listener = _listener!! + sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!) filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!) filterBtn = view.findViewById(R.id.format_filter) @@ -368,9 +381,10 @@ class FormatSelectionBottomSheetDialog(private val items: List, p FormatSorting.codec -> { val codecOrder = resources.getStringArray(R.array.video_codec_values).toMutableList() codecOrder.removeFirst() - chosenFormats.groupBy { format -> codecOrder.indexOfFirst { format.vcodec.startsWith(it) } } + chosenFormats.groupBy { format -> codecOrder.indexOfFirst { format.vcodec.matches("^(${it})(.+)?$".toRegex()) } } + .flatMap { - it.value.sortedBy { l -> l.filesize } + it.value.sortedByDescending { l -> l.filesize } } } FormatSorting.filesize -> chosenFormats diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ObserveSourcesBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ObserveSourcesBottomSheetDialog.kt index 9178cb17..3311aa3c 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ObserveSourcesBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ObserveSourcesBottomSheetDialog.kt @@ -47,6 +47,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup +import com.google.android.material.elevation.SurfaceColors import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout @@ -128,6 +129,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() { super.setupDialog(dialog, style) view = LayoutInflater.from(context).inflate(R.layout.observe_sources_bottom_sheet, null) dialog.setContentView(view) + dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity()) dialog.setOnShowListener { behavior = BottomSheetBehavior.from(view.parent as View) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt index fb843af0..d9fe1f73 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt @@ -59,6 +59,7 @@ import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip 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 com.yausername.youtubedl_android.YoutubeDL @@ -123,6 +124,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap super.setupDialog(dialog, style) val view = LayoutInflater.from(context).inflate(R.layout.result_card_details, null) dialog.setContentView(view) + dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity()) dialog.setOnShowListener { val behavior = BottomSheetBehavior.from(dialogView.parent as View) @@ -613,7 +615,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap } ActiveDownloadAdapter.ActiveDownloadAction.Resume -> { lifecycleScope.launch { - item.status = DownloadRepository.Status.PausedReQueued.toString() + item.status = DownloadRepository.Status.Queued.toString() withContext(Dispatchers.IO){ downloadViewModel.updateDownload(item) } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/SelectPlaylistItemsDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/SelectPlaylistItemsDialog.kt index 32e9f02f..aa39e30c 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/SelectPlaylistItemsDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/SelectPlaylistItemsDialog.kt @@ -29,6 +29,7 @@ import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.button.MaterialButton +import com.google.android.material.elevation.SurfaceColors import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.textfield.TextInputLayout import kotlinx.coroutines.Dispatchers @@ -77,6 +78,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O super.setupDialog(dialog, style) val view = LayoutInflater.from(context).inflate(R.layout.select_playlist_items, null) dialog.setContentView(view) + dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity()) dialog.setOnShowListener { behavior = BottomSheetBehavior.from(view.parent as View) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ActiveDownloadsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ActiveDownloadsFragment.kt index 3c489ba6..69f9f2f0 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ActiveDownloadsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ActiveDownloadsFragment.kt @@ -1,32 +1,55 @@ package com.deniscerri.ytdl.ui.downloads +import android.annotation.SuppressLint import android.app.Activity +import android.content.DialogInterface +import android.graphics.Canvas +import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem import android.view.View -import android.view.View.OnClickListener import android.view.ViewGroup import android.widget.RelativeLayout import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode +import androidx.core.os.bundleOf +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkQuery +import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter +import com.deniscerri.ytdl.ui.adapter.QueuedDownloadAdapter import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode +import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.NotificationUtil -import com.google.android.material.button.MaterialButton +import com.deniscerri.ytdl.util.UiUtil +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.chip.Chip +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.snackbar.Snackbar import com.yausername.youtubedl_android.YoutubeDL +import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest @@ -35,15 +58,17 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener, OnClickListener { +class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener { private var fragmentView: View? = null private var activity: Activity? = null private lateinit var downloadViewModel : DownloadViewModel + private lateinit var activeRecyclerView : RecyclerView private lateinit var activeDownloads : ActiveDownloadAdapter - lateinit var downloadItem: DownloadItem + private lateinit var notificationUtil: NotificationUtil - private lateinit var pauseResume: MaterialButton + private lateinit var pause: ExtendedFloatingActionButton + private lateinit var resume: ExtendedFloatingActionButton private lateinit var noResults: RelativeLayout private lateinit var workManager: WorkManager @@ -57,6 +82,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis return fragmentView } + @SuppressLint("NotifyDataSetChanged") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -65,75 +91,73 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] workManager = WorkManager.getInstance(requireContext()) - activeDownloads = - ActiveDownloadAdapter( - this, - requireActivity() - ) - + activeDownloads = ActiveDownloadAdapter(this,requireActivity()) activeRecyclerView = view.findViewById(R.id.download_recyclerview) activeRecyclerView.forceFastScrollMode() activeRecyclerView.adapter = activeDownloads activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) - pauseResume = view.findViewById(R.id.pause_resume) + + pause = view.findViewById(R.id.pause) + pause.isVisible = false + resume = view.findViewById(R.id.resume) noResults = view.findViewById(R.id.no_results) - pauseResume.setOnClickListener { - if (pauseResume.text == requireContext().getString(R.string.pause)){ - lifecycleScope.launch { - workManager.cancelAllWorkByTag("download") - pauseResume.isEnabled = false + pause.setOnClickListener { + lifecycleScope.launch { + workManager.cancelAllWorkByTag("download") + pause.isEnabled = false - // pause queued - withContext(Dispatchers.IO){ - downloadViewModel.getQueued() - }.forEach { - it.status = DownloadRepository.Status.QueuedPaused.toString() - downloadViewModel.updateDownload(it) - } - - // pause active - withContext(Dispatchers.IO){ - downloadViewModel.getActiveDownloads() - }.forEach { - cancelItem(it.id.toInt()) - it.status = DownloadRepository.Status.ActivePaused.toString() - downloadViewModel.updateDownload(it) - } - - - - activeDownloads.notifyDataSetChanged() - pauseResume.isEnabled = true + // pause queued + withContext(Dispatchers.IO){ + downloadViewModel.getQueued() + }.forEach { + it.status = DownloadRepository.Status.QueuedPaused.toString() + downloadViewModel.updateDownload(it) } - }else{ - lifecycleScope.launch { - pauseResume.isEnabled = false - val active = withContext(Dispatchers.IO){ - downloadViewModel.getActiveDownloads() - } + // pause active + withContext(Dispatchers.IO){ + downloadViewModel.getActiveDownloads() + }.forEach { + cancelItem(it.id.toInt()) + it.status = DownloadRepository.Status.ActivePaused.toString() + downloadViewModel.updateDownload(it) + } - val toQueue = active.filter { it.status == DownloadRepository.Status.ActivePaused.toString() }.toMutableList() + activeDownloads.notifyDataSetChanged() + pause.isEnabled = true + } + } + + resume.setOnClickListener { + lifecycleScope.launch { + resume.isEnabled = false + + val active = withContext(Dispatchers.IO){ + downloadViewModel.getActiveDownloads() + } + + val toQueue = active.filter { it.status == DownloadRepository.Status.ActivePaused.toString() }.toMutableList() + runBlocking { toQueue.forEach { it.status = DownloadRepository.Status.Queued.toString() downloadViewModel.updateDownload(it) } - - runBlocking { - downloadViewModel.queueDownloads(listOf()) - } - - val queuedItems = withContext(Dispatchers.IO){ - downloadViewModel.getQueued() - } - queuedItems.map { - it.status = DownloadRepository.Status.Queued.toString() - downloadViewModel.updateDownload(it) - } - - pauseResume.isEnabled = true } + + runBlocking { + downloadViewModel.startDownloadWorker(listOf()) + } + + val queuedItems = withContext(Dispatchers.IO){ + downloadViewModel.getQueued() + } + queuedItems.map { + it.status = DownloadRepository.Status.Queued.toString() + downloadViewModel.updateDownload(it) + } + + resume.isEnabled = true } } @@ -160,10 +184,23 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis } } + + lifecycleScope.launch { + downloadViewModel.activeAndActivePausedDownloadsCount.collectLatest { + noResults.isVisible = it == 0 + } + } + lifecycleScope.launch { downloadViewModel.activeDownloadsCount.collectLatest { - delay(200) - noResults.visibility = if (it == 0) View.VISIBLE else View.GONE + pause.isVisible = it > 0 + } + } + + + lifecycleScope.launch { + downloadViewModel.pausedDownloadsCount.collectLatest { + resume.isVisible = it > 0 } } @@ -171,19 +208,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis downloadViewModel.activeDownloads.collectLatest { delay(100) activeDownloads.submitList(it) - - if (it.size > 1){ - pauseResume.visibility = View.VISIBLE - if (it.all { l -> l.status == DownloadRepository.Status.ActivePaused.toString() }){ - pauseResume.text = requireContext().getString(R.string.resume) - }else{ - pauseResume.text = requireContext().getString(R.string.pause) - } - }else{ - pauseResume.visibility = View.GONE - } } - } } @@ -210,46 +235,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis cancelDownload(itemID) } - override fun onPauseClick(itemID: Long, action: ActiveDownloadAdapter.ActiveDownloadAction, position: Int) { - lifecycleScope.launch { - val item = withContext(Dispatchers.IO){ - downloadViewModel.getItemByID(itemID) - } - - when(action){ - ActiveDownloadAdapter.ActiveDownloadAction.Pause -> { - lifecycleScope.launch { - cancelItem(itemID.toInt()) - item.status = DownloadRepository.Status.ActivePaused.toString() - withContext(Dispatchers.IO){ - downloadViewModel.updateDownload(item) - } - } - } - ActiveDownloadAdapter.ActiveDownloadAction.Resume -> { - lifecycleScope.launch { - item.status = DownloadRepository.Status.PausedReQueued.toString() - withContext(Dispatchers.IO){ - downloadViewModel.updateDownload(item) - } - - runBlocking { - downloadViewModel.queueDownloads(listOf(item)) - } - - withContext(Dispatchers.IO){ - downloadViewModel.getQueued().filter { it.status != DownloadRepository.Status.Queued.toString() }.forEach { - it.status = DownloadRepository.Status.Queued.toString() - downloadViewModel.updateDownload(it) - } - } - } - } - } - - } - } - override fun onOutputClick(item: DownloadItem) { if (item.logID != null && item.logID != 0L) { val bundle = Bundle() @@ -279,7 +264,4 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis } } } - - override fun onClick(p0: View?) { - } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/DownloadQueueMainFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/DownloadQueueMainFragment.kt index 27e4dcb4..ab0d3d41 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/DownloadQueueMainFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/DownloadQueueMainFragment.kt @@ -4,6 +4,8 @@ import android.content.DialogInterface import android.content.SharedPreferences import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup @@ -15,6 +17,7 @@ import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import androidx.work.WorkManager +import com.afollestad.materialdialogs.utils.MDUtil.inflate import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.repository.DownloadRepository @@ -69,7 +72,7 @@ class DownloadQueueMainFragment : Fragment(){ overScrollMode = View.OVER_SCROLL_NEVER } - val fragments = mutableListOf(ActiveDownloadsFragment(), QueuedDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment(), SavedDownloadsFragment()) + val fragments = mutableListOf(ActiveDownloadsFragment(), QueuedDownloadsFragment(), ScheduledDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment(), SavedDownloadsFragment()) fragmentAdapter = DownloadListFragmentAdapter( childFragmentManager, @@ -84,9 +87,10 @@ class DownloadQueueMainFragment : Fragment(){ when (position) { 0 -> tab.text = getString(R.string.running) 1 -> tab.text = getString(R.string.in_queue) - 2 -> tab.text = getString(R.string.cancelled) - 3 -> tab.text = getString(R.string.errored) - 4 -> tab.text = getString(R.string.saved) + 2 -> tab.text = getString(R.string.scheduled) + 3 -> tab.text = getString(R.string.cancelled) + 4 -> tab.text = getString(R.string.errored) + 5 -> tab.text = getString(R.string.saved) } }.attach() @@ -105,6 +109,7 @@ class DownloadQueueMainFragment : Fragment(){ viewPager2.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { tabLayout.selectTab(tabLayout.getTabAt(position)) + initMenu() } }) mainActivity.hideBottomNavigation() @@ -133,32 +138,41 @@ class DownloadQueueMainFragment : Fragment(){ } } lifecycleScope.launch { - downloadViewModel.cancelledDownloadsCount.collectLatest { + downloadViewModel.scheduledDownloadsCount.collectLatest { tabLayout.getTabAt(2)?.apply { createBadge(it) } } } lifecycleScope.launch { - downloadViewModel.erroredDownloadsCount.collectLatest { + downloadViewModel.cancelledDownloadsCount.collectLatest { tabLayout.getTabAt(3)?.apply { createBadge(it) } } } lifecycleScope.launch { - downloadViewModel.savedDownloadsCount.collectLatest { + downloadViewModel.erroredDownloadsCount.collectLatest { tabLayout.getTabAt(4)?.apply { removeBadge() if (it > 0) createBadge(it) } } } + lifecycleScope.launch { + downloadViewModel.savedDownloadsCount.collectLatest { + tabLayout.getTabAt(5)?.apply { + removeBadge() + if (it > 0) createBadge(it) + } + } + } } } private fun initMenu() { + topAppBar.setOnMenuItemClickListener { m: MenuItem -> try{ when(m.itemId){ @@ -172,6 +186,11 @@ class DownloadQueueMainFragment : Fragment(){ downloadViewModel.deleteCancelled() } } + R.id.clear_scheduled -> { + showDeleteDialog { + downloadViewModel.deleteScheduled() + } + } R.id.clear_errored -> { showDeleteDialog { downloadViewModel.deleteErrored() @@ -185,11 +204,12 @@ class DownloadQueueMainFragment : Fragment(){ R.id.copy_urls -> { lifecycleScope.launch { val tabStatus = mapOf( - 0 to listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused, DownloadRepository.Status.PausedReQueued), - 1 to listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.Queued), - 2 to listOf(DownloadRepository.Status.Cancelled), - 3 to listOf(DownloadRepository.Status.Error), - 4 to listOf(DownloadRepository.Status.Saved), + 0 to listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused), + 1 to listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused), + 2 to listOf(DownloadRepository.Status.Scheduled), + 3 to listOf(DownloadRepository.Status.Cancelled), + 4 to listOf(DownloadRepository.Status.Error), + 5 to listOf(DownloadRepository.Status.Saved), ) tabStatus[tabLayout.selectedTabPosition]?.apply { val urls = withContext(Dispatchers.IO){ @@ -233,9 +253,4 @@ class DownloadQueueMainFragment : Fragment(){ } } } - - - companion object { - private const val TAG = "DownloadQueueActivity" - } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/QueuedDownloadsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/QueuedDownloadsFragment.kt index df1a9d16..6be46c1e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/QueuedDownloadsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/QueuedDownloadsFragment.kt @@ -1,22 +1,26 @@ package com.deniscerri.ytdl.ui.downloads +import android.animation.AnimatorSet import android.annotation.SuppressLint import android.app.Activity import android.content.DialogInterface import android.graphics.Canvas import android.graphics.Color import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.view.animation.AccelerateDecelerateInterpolator import android.widget.RelativeLayout import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.core.os.bundleOf +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -31,7 +35,7 @@ import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel -import com.deniscerri.ytdl.ui.adapter.GenericDownloadAdapter +import com.deniscerri.ytdl.ui.adapter.QueuedDownloadAdapter import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode import com.deniscerri.ytdl.util.Extensions.toListString @@ -39,6 +43,7 @@ import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.UiUtil import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.card.MaterialCardView import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar @@ -51,15 +56,16 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener { +class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickListener { private var fragmentView: View? = null private var activity: Activity? = null private lateinit var downloadViewModel : DownloadViewModel private lateinit var queuedRecyclerView : RecyclerView - private lateinit var adapter : GenericDownloadAdapter + private lateinit var adapter : QueuedDownloadAdapter private lateinit var noResults : RelativeLayout private lateinit var notificationUtil: NotificationUtil private lateinit var fileSize: TextView + private lateinit var dragHandleToggle: TextView private var totalSize: Int = 0 private var actionMode : ActionMode? = null @@ -75,21 +81,20 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi return fragmentView } - @SuppressLint("SetTextI18n") + @SuppressLint("SetTextI18n", "RestrictedApi") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) fileSize = view.findViewById(R.id.filesize) - adapter = - GenericDownloadAdapter( - this, - requireActivity() - ) + dragHandleToggle = view.findViewById(R.id.drag) + val itemTouchHelper = ItemTouchHelper(queuedDragDropHelper) + adapter = QueuedDownloadAdapter(this, requireActivity(), itemTouchHelper) noResults = view.findViewById(R.id.no_results) queuedRecyclerView = view.findViewById(R.id.download_recyclerview) queuedRecyclerView.forceFastScrollMode() queuedRecyclerView.adapter = adapter queuedRecyclerView.enableFastScroll() + itemTouchHelper.attachToRecyclerView(queuedRecyclerView) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("queued")){ @@ -126,83 +131,12 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused)).observe(viewLifecycleOwner){ totalSize = it - noResults.visibility = if (it == 0) View.VISIBLE else View.GONE + noResults.isVisible = it == 0 + dragHandleToggle.isVisible = it > 1 } - } - override fun onActionButtonClick(itemID: Long) { - removeItem(itemID) - } - - override fun onCardClick(itemID: Long) { - lifecycleScope.launch { - val item = withContext(Dispatchers.IO){ - downloadViewModel.getItemByID(itemID) - } - - UiUtil.showDownloadItemDetailsCard( - item, - requireActivity(), - DownloadRepository.Status.valueOf(item.status), - removeItem = { it: DownloadItem, sheet: BottomSheetDialog -> - sheet.hide() - removeItem(it.id) - }, - downloadItem = { - downloadViewModel.deleteDownload(it.id) - it.downloadStartTime = 0 - WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString()) - runBlocking { - downloadViewModel.queueDownloads(listOf(it)) - } - }, - longClickDownloadButton = { - findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf( - Pair("downloadItem", it), - Pair("result", downloadViewModel.createResultItemFromDownload(it)), - Pair("type", it.type) - )) - }, - scheduleButtonClick = {downloadItem -> - UiUtil.showDatePicker(parentFragmentManager) { - Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show() - downloadViewModel.deleteDownload(downloadItem.id) - downloadItem.downloadStartTime = it.timeInMillis - WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString()) - runBlocking { - downloadViewModel.queueDownloads(listOf(downloadItem)) - } - } - } - ) - } - } - - override fun onCardSelect(isChecked: Boolean, position: Int) { - lifecycleScope.launch { - val selectedObjects = adapter.getSelectedObjectsCount(totalSize) - if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) - val now = System.currentTimeMillis() - actionMode?.apply { - if (selectedObjects == 0){ - this.finish() - }else{ - this.title = "$selectedObjects ${getString(R.string.selected)}" - this.menu.findItem(R.id.download).isVisible = withContext(Dispatchers.IO){ - downloadViewModel.checkAllQueuedItemsAreScheduledAfterNow(adapter.checkedItems.toList(), adapter.inverted, now) - } - - this.menu.findItem(R.id.up).isVisible = position > 0 - this.menu.findItem(R.id.select_between).isVisible = false - if(selectedObjects == 2){ - val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it } - val idsInMiddle = withContext(Dispatchers.IO){ - downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused).toListString()) - } - this.menu.findItem(R.id.select_between).isVisible = idsInMiddle.isNotEmpty() - } - } - } + dragHandleToggle.setOnClickListener { + adapter.toggleShowDragHandle() } } @@ -255,7 +189,8 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi lifecycleScope.launch { val selectedIDs = getSelectedIDs().sortedBy { it } val idsInMiddle = withContext(Dispatchers.IO){ - downloadViewModel.getQueuedIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last()) + downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf( + DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused).toListString()) }.toMutableList() idsInMiddle.addAll(selectedIDs) if (idsInMiddle.isNotEmpty()){ @@ -324,6 +259,17 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi } true } + R.id.down -> { + lifecycleScope.launch { + val selectedObjects = getSelectedIDs() + adapter.clearCheckedItems() + withContext(Dispatchers.IO){ + downloadViewModel.putAtBottomOfQueue(selectedObjects) + } + actionMode?.finish() + } + true + } R.id.copy_urls -> { lifecycleScope.launch { val selectedObjects = getSelectedIDs() @@ -420,4 +366,186 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi ) } } + + var movedToNewPositionID = 0L + private val queuedDragDropHelper: ItemTouchHelper.SimpleCallback = + object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val fromPosition = viewHolder.bindingAdapterPosition + val toPosition = target.bindingAdapterPosition + movedToNewPositionID = target.itemView.tag.toString().toLong() + adapter.notifyItemMoved(fromPosition, toPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + } + + override fun onSelectedChanged( + viewHolder: RecyclerView.ViewHolder?, + actionState: Int + ) { + super.onSelectedChanged(viewHolder, actionState) + if (ItemTouchHelper.ACTION_STATE_DRAG == actionState) { + /** + * Change alpha, scale and elevation on drag. + */ + (viewHolder?.itemView as? MaterialCardView)?.also { + AnimatorSet().apply { + this.duration = 100L + this.interpolator = AccelerateDecelerateInterpolator() + + playTogether( + UiUtil.getAlphaAnimator(it, 0.7f), + UiUtil.getScaleXAnimator(it, 1.02f), + UiUtil.getScaleYAnimator(it, 1.02f), + UiUtil.getElevationAnimator(it, R.dimen.elevation_6dp) + ) + }.start() + } + } + } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) { + super.clearView(recyclerView, viewHolder) + /** + * Clear alpha, scale and elevation after drag/swipe + */ + (viewHolder.itemView as? MaterialCardView)?.also { + AnimatorSet().apply { + this.duration = 100L + this.interpolator = AccelerateDecelerateInterpolator() + + playTogether( + UiUtil.getAlphaAnimator(it, 1f), + UiUtil.getScaleXAnimator(it, 1f), + UiUtil.getScaleYAnimator(it, 1f), + UiUtil.getElevationAnimator(it, R.dimen.elevation_2dp) + ) + }.start() + } + + downloadViewModel.putAtPosition(viewHolder.itemView.tag.toString().toLong(), movedToNewPositionID) + } + + override fun isLongPressDragEnabled(): Boolean { + return false + } + } + + override fun onMoveQueuedItemToTop(itemID: Long) { + lifecycleScope.launch { + downloadViewModel.putAtTopOfQueue(listOf(itemID)) + } + } + + override fun onMoveQueuedItemToBottom(itemID: Long) { + lifecycleScope.launch { + downloadViewModel.putAtBottomOfQueue(listOf(itemID)) + } + } + + override fun onQueuedCardClick(itemID: Long) { + lifecycleScope.launch { + val item = withContext(Dispatchers.IO){ + downloadViewModel.getItemByID(itemID) + } + + UiUtil.showDownloadItemDetailsCard( + item, + requireActivity(), + DownloadRepository.Status.valueOf(item.status), + removeItem = { it: DownloadItem, sheet: BottomSheetDialog -> + sheet.hide() + removeItem(it.id) + }, + downloadItem = { + downloadViewModel.deleteDownload(it.id) + it.downloadStartTime = 0 + WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString()) + runBlocking { + downloadViewModel.queueDownloads(listOf(it)) + } + }, + longClickDownloadButton = { + findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf( + Pair("downloadItem", it), + Pair("result", downloadViewModel.createResultItemFromDownload(it)), + Pair("type", it.type) + ) + ) + }, + scheduleButtonClick = {downloadItem -> + UiUtil.showDatePicker(parentFragmentManager) { + Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show() + downloadViewModel.deleteDownload(downloadItem.id) + downloadItem.downloadStartTime = it.timeInMillis + WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString()) + runBlocking { + downloadViewModel.queueDownloads(listOf(downloadItem)) + } + } + } + ) + } + } + + override fun onQueuedCardSelect(isChecked: Boolean, position: Int) { + lifecycleScope.launch { + val selectedObjects = adapter.getSelectedObjectsCount(totalSize) + if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) + val now = System.currentTimeMillis() + actionMode?.apply { + if (selectedObjects == 0){ + this.finish() + }else{ + this.title = "$selectedObjects ${getString(R.string.selected)}" + this.menu.findItem(R.id.download).isVisible = withContext(Dispatchers.IO){ + downloadViewModel.checkAllQueuedItemsAreScheduledAfterNow(adapter.checkedItems.toList(), adapter.inverted, now) + } + + this.menu.findItem(R.id.up).isVisible = position > 0 + this.menu.findItem(R.id.down).isVisible = position < totalSize + this.menu.findItem(R.id.select_between).isVisible = false + if(selectedObjects == 2){ + val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it } + val idsInMiddle = withContext(Dispatchers.IO){ + downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused).toListString()) + } + this.menu.findItem(R.id.select_between).isVisible = idsInMiddle.isNotEmpty() + } + } + } + } + } + + override fun onQueuedCancelClick(itemID: Long) { + cancelDownload(itemID) + } + + private fun cancelDownload(itemID: Long){ + lifecycleScope.launch { + cancelItem(itemID.toInt()) + withContext(Dispatchers.IO){ + downloadViewModel.getItemByID(itemID) + }.let { + it.status = DownloadRepository.Status.Cancelled.toString() + withContext(Dispatchers.IO){ + downloadViewModel.updateDownload(it) + } + } + } + } + + private fun cancelItem(id: Int){ + YoutubeDL.getInstance().destroyProcessById(id.toString()) + notificationUtil.cancelDownloadNotification(id) + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ScheduledDownloadsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ScheduledDownloadsFragment.kt new file mode 100644 index 00000000..63bbb6cb --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ScheduledDownloadsFragment.kt @@ -0,0 +1,401 @@ +package com.deniscerri.ytdl.ui.downloads + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.DialogInterface +import android.content.SharedPreferences +import android.graphics.Canvas +import android.graphics.Color +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.RelativeLayout +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import androidx.work.WorkManager +import com.afollestad.materialdialogs.utils.MDUtil.getStringArray +import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.database.models.DownloadItem +import com.deniscerri.ytdl.database.repository.DownloadRepository +import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel +import com.deniscerri.ytdl.ui.adapter.GenericDownloadAdapter +import com.deniscerri.ytdl.ui.adapter.ScheduledDownloadAdapter +import com.deniscerri.ytdl.util.Extensions.enableFastScroll +import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode +import com.deniscerri.ytdl.util.Extensions.toListString +import com.deniscerri.ytdl.util.UiUtil +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.color.MaterialColors +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + + +class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemClickListener { + private var fragmentView: View? = null + private var activity: Activity? = null + private lateinit var downloadViewModel : DownloadViewModel + private lateinit var scheduledRecyclerView : RecyclerView + private lateinit var preferences : SharedPreferences + private lateinit var adapter : ScheduledDownloadAdapter + private lateinit var noResults : RelativeLayout + private var actionMode : ActionMode? = null + private var totalSize = 0 + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + fragmentView = inflater.inflate(R.layout.generic_list, container, false) + activity = getActivity() + downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] + preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + return fragmentView + } + + @SuppressLint("RestrictedApi") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + adapter = + ScheduledDownloadAdapter( + this, + requireActivity() + ) + + noResults = view.findViewById(R.id.no_results) + scheduledRecyclerView = view.findViewById(R.id.download_recyclerview) + scheduledRecyclerView.forceFastScrollMode() + scheduledRecyclerView.adapter = adapter + scheduledRecyclerView.enableFastScroll() + if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("scheduled")){ + val itemTouchHelper = ItemTouchHelper(simpleCallback) + itemTouchHelper.attachToRecyclerView(scheduledRecyclerView) + } + scheduledRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) + + + lifecycleScope.launch { + downloadViewModel.scheduledDownloads.collectLatest { + adapter.submitData(it) + } + } + + lifecycleScope.launch { + downloadViewModel.scheduledDownloadsCount.collectLatest { + totalSize = it + noResults.visibility = if (it == 0) View.VISIBLE else View.GONE + } + } + + } + + override fun onActionButtonClick(itemID: Long) { + lifecycleScope.launch { + runCatching { + val item = withContext(Dispatchers.IO){ + downloadViewModel.getItemByID(itemID) + } + withContext(Dispatchers.IO){ + item.downloadStartTime = 0 + downloadViewModel.queueDownloads(listOf(item), true) + } + }.onFailure { + Toast.makeText(requireContext(), it.message, Toast.LENGTH_LONG).show() + } + } + } + + override fun onCardClick(itemID: Long) { + lifecycleScope.launch { + val item = withContext(Dispatchers.IO){ + downloadViewModel.getItemByID(itemID) + } + + UiUtil.showDownloadItemDetailsCard( + item, + requireActivity(), + DownloadRepository.Status.valueOf(item.status), + removeItem = { it: DownloadItem, sheet: BottomSheetDialog -> + sheet.hide() + removeItem(it, sheet) + }, + downloadItem = { + downloadViewModel.deleteDownload(it.id) + it.downloadStartTime = 0 + WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString()) + runBlocking { + downloadViewModel.queueDownloads(listOf(it)) + } + }, + longClickDownloadButton = { + findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf( + Pair("downloadItem", it), + Pair("result", downloadViewModel.createResultItemFromDownload(it)), + Pair("type", it.type) + ) + ) + }, + scheduleButtonClick = {downloadItem -> + UiUtil.showDatePicker(parentFragmentManager) { + Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show() + downloadViewModel.deleteDownload(downloadItem.id) + downloadItem.downloadStartTime = it.timeInMillis + WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString()) + runBlocking { + downloadViewModel.queueDownloads(listOf(downloadItem)) + } + } + } + ) + } + } + + override fun onCardSelect(isChecked: Boolean, position: Int) { + lifecycleScope.launch { + val selectedObjects = adapter.getSelectedObjectsCount(totalSize) + if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) + actionMode?.apply { + if (selectedObjects == 0){ + this.finish() + }else{ + this.title = "$selectedObjects ${getString(R.string.selected)}" + this.menu.findItem(R.id.select_between).isVisible = false + if (selectedObjects == 2){ + val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it } + val idsInMiddle = withContext(Dispatchers.IO){ + downloadViewModel.getScheduledIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last()) + } + this.menu.findItem(R.id.select_between).isVisible = idsInMiddle.isNotEmpty() + } + } + } + + if (isChecked) { + if (actionMode == null){ + actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) + }else{ + actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}" + } + } + else { + actionMode?.title = "$selectedObjects ${getString(R.string.selected)}" + if (selectedObjects == 0){ + actionMode?.finish() + } + } + } + } + + + private fun removeItem(item: DownloadItem, bottomSheet: BottomSheetDialog?){ + bottomSheet?.hide() + val deleteDialog = MaterialAlertDialogBuilder(requireContext()) + deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!") + deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } + deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> + downloadViewModel.deleteDownload(item.id) + } + deleteDialog.show() + } + + + private val contextualActionBar = object : ActionMode.Callback { + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + mode!!.menuInflater.inflate(R.menu.queued_menu_context, menu) + mode.title = "${adapter.getSelectedObjectsCount(totalSize)} ${getString(R.string.selected)}" + return true + } + + override fun onPrepareActionMode( + mode: ActionMode?, + menu: Menu? + ): Boolean { + return false + } + + override fun onActionItemClicked( + mode: ActionMode?, + item: MenuItem? + ): Boolean { + return when (item!!.itemId) { + R.id.select_between -> { + lifecycleScope.launch { + val selectedIDs = getSelectedIDs().sortedBy { it } + val idsInMiddle = withContext(Dispatchers.IO){ + downloadViewModel.getScheduledIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last()) + }.toMutableList() + idsInMiddle.addAll(selectedIDs) + if (idsInMiddle.isNotEmpty()){ + adapter.checkMultipleItems(idsInMiddle) + actionMode?.title = "${idsInMiddle.count()} ${getString(R.string.selected)}" + } + mode?.menu?.findItem(R.id.select_between)?.isVisible = false + } + true + } + R.id.delete_results -> { + val deleteDialog = MaterialAlertDialogBuilder(requireContext()) + deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) + deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } + deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> + lifecycleScope.launch { + val selectedObjects = getSelectedIDs() + adapter.clearCheckedItems() + downloadViewModel.deleteAllWithID(selectedObjects) + actionMode?.finish() + } + } + deleteDialog.show() + true + } + R.id.download -> { + lifecycleScope.launch { + val selectedObjects = getSelectedIDs() + adapter.clearCheckedItems() + for (id in selectedObjects){ + WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString()) + } + withContext(Dispatchers.IO) { + downloadViewModel.resetScheduleTimeForItemsAndStartDownload(selectedObjects) + } + actionMode?.finish() + } + true + } + R.id.select_all -> { + adapter.checkAll() + mode?.title = getString(R.string.all_items_selected) + true + } + R.id.invert_selected -> { + adapter.invertSelected() + val selectedObjects = adapter.getSelectedObjectsCount(totalSize) + actionMode!!.title = "$selectedObjects ${getString(R.string.selected)}" + if (selectedObjects == 0) actionMode?.finish() + true + } + R.id.copy_urls -> { + lifecycleScope.launch { + val selectedObjects = getSelectedIDs() + val urls = withContext(Dispatchers.IO){ + downloadViewModel.getURLsByIds(selectedObjects) + } + UiUtil.copyToClipboard(urls.joinToString("\n"), requireActivity()) + actionMode?.finish() + } + true + } + else -> false + } + } + + override fun onDestroyActionMode(mode: ActionMode?) { + actionMode = null + adapter.clearCheckedItems() + } + + suspend fun getSelectedIDs() : List{ + return if (adapter.inverted || adapter.checkedItems.isEmpty()){ + withContext(Dispatchers.IO){ + downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems.toList(), listOf( + DownloadRepository.Status.Scheduled)) + } + }else{ + adapter.checkedItems.toList() + } + } + } + + private var simpleCallback: ItemTouchHelper.SimpleCallback = + object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { + override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder + ): Boolean { + return false + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val itemID = viewHolder.itemView.tag.toString().toLong() + val position = viewHolder.bindingAdapterPosition + when (direction) { + ItemTouchHelper.LEFT -> { + lifecycleScope.launch { + val deletedItem = withContext(Dispatchers.IO){ + downloadViewModel.getItemByID(itemID) + } + downloadViewModel.deleteDownload(deletedItem.id) + Snackbar.make(scheduledRecyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG) + .setAction(getString(R.string.undo)) { + downloadViewModel.insert(deletedItem) + }.show() + } + } + ItemTouchHelper.RIGHT -> { + onActionButtonClick(itemID) + adapter.notifyItemChanged(position) + } + } + } + + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean + ) { + RecyclerViewSwipeDecorator.Builder( + requireContext(), + c, + recyclerView, + viewHolder, + dX, + dY, + actionState, + isCurrentlyActive + ) + .addSwipeLeftBackgroundColor(Color.RED) + .addSwipeLeftActionIcon(R.drawable.baseline_delete_24) + .addSwipeRightBackgroundColor( + MaterialColors.getColor( + requireContext(), + R.attr.colorOnSurfaceInverse,Color.TRANSPARENT + ) + ) + .addSwipeRightActionIcon(R.drawable.ic_refresh) + .create() + .decorate() + super.onChildDraw( + c, + recyclerView, + viewHolder!!, + dX, + dY, + actionState, + isCurrentlyActive + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/CookiesFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/CookiesFragment.kt index 1798b185..f217107c 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/CookiesFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/CookiesFragment.kt @@ -107,6 +107,7 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener { val useCookiesPref = preferences.getBoolean("use_cookies", false) useCookies.isChecked = useCookiesPref + useCookies.jumpDrawablesToCurrentState() newCookie.isEnabled = useCookiesPref } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogFragment.kt index 1f8c45ee..bbe91047 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogFragment.kt @@ -170,6 +170,7 @@ class DownloadLogFragment : Fragment() { if (logItem != null){ if (logItem.content.isNotBlank()) { content.setText(logItem.content, TextView.BufferType.SPANNABLE) + bottomAppBar?.menu?.get(1)?.isVisible = contentScrollView.canScrollVertically(1) } if (!bottomAppBar.menu.children.first { it.itemId == R.id.scroll_down }.isVisible){ content.scrollTo(0, content.height) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DownloadSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DownloadSettingsFragment.kt index fb945621..8ab8ea0f 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DownloadSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DownloadSettingsFragment.kt @@ -1,7 +1,9 @@ package com.deniscerri.ytdl.ui.more.settings +import android.content.DialogInterface import android.content.Intent import android.os.Bundle +import androidx.core.content.edit import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceManager @@ -14,6 +16,8 @@ import com.deniscerri.ytdl.R import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.DownloadWorker +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.materialswitch.MaterialSwitch import java.util.Calendar import java.util.concurrent.TimeUnit @@ -25,6 +29,16 @@ class DownloadSettingsFragment : BaseSettingsFragment() { setPreferencesFromResource(R.xml.downloading_preferences, rootKey) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + findPreference("prevent_duplicate_downloads")?.apply { + //TODO transitioning code, delete after couple releases + if (preferences.getBoolean("download_archive", false)){ + preferences.edit(commit = true){ + putBoolean("download_archive", false).apply() + putString("prevent_duplicate_downloads", "download_archive") + } + } + } + val rememberDownloadType = findPreference("remember_download_type") val downloadType = findPreference("preferred_download_type") downloadType?.isEnabled = rememberDownloadType?.isChecked == false diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt index 7bd52900..232e85cd 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt @@ -4,11 +4,13 @@ import android.app.Activity import android.content.Intent import android.content.SharedPreferences import android.net.Uri +import android.os.Build import android.os.Build.VERSION import android.os.Bundle import android.os.Environment import android.provider.Settings import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi import androidx.preference.Preference import androidx.preference.PreferenceManager import androidx.preference.SwitchPreferenceCompat @@ -112,15 +114,22 @@ class FolderSettingsFragment : BaseSettingsFragment() { commandPathResultLauncher.launch(intent) true } - accessAllFiles!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) - val uri = Uri.parse("package:" + requireContext().packageName) - intent.data = uri - startActivity(intent) - true - } + if(VERSION.SDK_INT >= 30){ + accessAllFiles!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + val uri = Uri.parse("package:" + requireContext().packageName) + intent.data = uri + startActivity(intent) + true + } + } + if (noFragments!!.isChecked) { + editor.putBoolean("keep_cache", false).apply() + keepFragments!!.isChecked = false + keepFragments!!.isEnabled = false + } noFragments!!.setOnPreferenceChangeListener { _, newValue -> if(newValue as Boolean){ editor.putBoolean("keep_cache", false).apply() diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt index 10f68ca2..36584331 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt @@ -115,7 +115,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() { showTerminalShareIcon!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { pref: Preference?, _: Any -> val packageManager = requireContext().packageManager - val aliasComponentName = ComponentName(requireContext(), "com.deniscerri.ytdlnis.terminalShareAlias") + val aliasComponentName = ComponentName(requireContext(), "com.deniscerri.ytdl.terminalShareAlias") if ((pref as SwitchPreferenceCompat).isChecked){ packageManager.setComponentEnabledSetting(aliasComponentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, diff --git a/app/src/main/java/com/deniscerri/ytdl/util/CrashListener.kt b/app/src/main/java/com/deniscerri/ytdl/util/CrashListener.kt index b4438e8f..6e14393a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/CrashListener.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/CrashListener.kt @@ -1,6 +1,7 @@ package com.deniscerri.ytdl.util import android.content.Context +import android.util.Log import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.LogItem @@ -15,6 +16,7 @@ class CrashListener(private val context: Context) : Thread.UncaughtExceptionHand override fun uncaughtException(p0: Thread, p1: Throwable) { p1.message?.apply { + Log.e("ERROR", this) CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { createLog(this@apply) } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt b/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt index 04edce70..01cf18b6 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt @@ -1,5 +1,6 @@ package com.deniscerri.ytdl.util +import android.R.color import android.animation.ValueAnimator import android.annotation.SuppressLint import android.app.Dialog @@ -9,7 +10,9 @@ import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Outline -import android.graphics.drawable.Drawable +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.OvalShape import android.media.MediaMetadataRetriever @@ -27,6 +30,7 @@ import android.widget.ImageView import android.widget.TextView import androidx.annotation.Px import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import androidx.lifecycle.withStarted @@ -335,10 +339,13 @@ object Extensions { return cookie.toString() } - fun ObserveSourcesItem.calculateNextTime() : Long { + fun ObserveSourcesItem.calculateNextTimeForObserving() : Long { val item = this + val now = System.currentTimeMillis() Calendar.getInstance().apply { - if (item.everyCategory != com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory.HOUR){ + timeInMillis = item.startsTime + + if (item.everyCategory != EveryCategory.HOUR){ val hourMin = Calendar.getInstance() hourMin.timeInMillis = item.everyTime @@ -346,38 +353,40 @@ object Extensions { set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE)) } - when(item.everyCategory){ - EveryCategory.HOUR -> { - add(Calendar.HOUR, item.everyNr) - } - EveryCategory.DAY -> { - add(Calendar.DAY_OF_MONTH, item.everyNr) - } - EveryCategory.WEEK -> { - item.weeklyConfig?.apply { - if (this.weekDays.isEmpty()){ - add(Calendar.DAY_OF_MONTH, 7 * item.everyNr) - }else{ - var weekDayNr = get(Calendar.DAY_OF_WEEK) - 1 - if (weekDayNr == 0) weekDayNr = 7 - val followingWeekDay = this.weekDays.firstOrNull { it > weekDayNr } - if (followingWeekDay == null){ - add(Calendar.DAY_OF_MONTH, this.weekDays.minBy { it } + (7 - weekDayNr)) - item.everyNr-- - }else{ - add(Calendar.DAY_OF_MONTH, followingWeekDay.toInt() - weekDayNr) - } - - if (item.everyNr > 1){ + while (timeInMillis < now){ + when(item.everyCategory){ + EveryCategory.HOUR -> { + add(Calendar.HOUR, item.everyNr) + } + EveryCategory.DAY -> { + add(Calendar.DAY_OF_MONTH, item.everyNr) + } + EveryCategory.WEEK -> { + item.weeklyConfig?.apply { + if (this.weekDays.isEmpty()){ add(Calendar.DAY_OF_MONTH, 7 * item.everyNr) + }else{ + var weekDayNr = get(Calendar.DAY_OF_WEEK) - 1 + if (weekDayNr == 0) weekDayNr = 7 + val followingWeekDay = this.weekDays.firstOrNull { it > weekDayNr } + if (followingWeekDay == null){ + add(Calendar.DAY_OF_MONTH, this.weekDays.minBy { it } + (7 - weekDayNr)) + item.everyNr-- + }else{ + add(Calendar.DAY_OF_MONTH, followingWeekDay.toInt() - weekDayNr) + } + + if (item.everyNr > 1){ + add(Calendar.DAY_OF_MONTH, 7 * item.everyNr) + } } } } - } - EveryCategory.MONTH -> { - add(Calendar.MONTH, item.everyNr) - item.monthlyConfig?.apply { - set(Calendar.DAY_OF_MONTH, this.everyMonthDay) + EveryCategory.MONTH -> { + add(Calendar.MONTH, item.everyNr) + item.monthlyConfig?.apply { + set(Calendar.DAY_OF_MONTH, this.everyMonthDay) + } } } } @@ -392,7 +401,14 @@ object Extensions { drawable!!.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888 ) + + val paint = Paint() + val colorValue = TypedValue() + context.theme.resolveAttribute(android.R.attr.colorActivatedHighlight, colorValue, true) + paint.setColorFilter(PorterDuffColorFilter(colorValue.data, PorterDuff.Mode.SRC_IN)) + val canvas = Canvas(bitmap) + DrawableCompat.setTint(drawable, colorValue.data) drawable.setBounds(0, 0, canvas.width, canvas.height) drawable.draw(canvas) return bitmap diff --git a/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt index 3853149b..512c0b9b 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt @@ -763,6 +763,14 @@ class InfoUtil(private val context: Context) { format.put("filesize", size) }catch (ignored: Exception){} } + + if (format.has("filesize_approx")){ + if (format.get("filesize_approx") == "None"){ + format.remove("filesize_approx") + format.put("filesize_approx", 0) + } + } + val formatProper = Gson().fromJson(format.toString(), Format::class.java) if (formatProper.format_note == null) continue @@ -1089,7 +1097,7 @@ class InfoUtil(private val context: Context) { } } - if (sharedPreferences.getBoolean("download_archive", false)){ + if (sharedPreferences.getString("prevent_duplicate_downloads", "")!! == "download_archive"){ request.addOption("--download-archive", FileUtil.getDownloadArchivePath(context)) } @@ -1152,7 +1160,7 @@ class InfoUtil(private val context: Context) { if (embedMetadata){ request.addOption("--embed-metadata") request.addOption("--parse-metadata", "artist:(?P[^,]+)") - request.addOption("--parse-metadata", "%(album_artist,meta_album_artist,uploader)s:%(album_artist)s") + request.addOption("--parse-metadata", "%(album_artist,meta_album_artist,uploader|)s:%(album_artist)s") request.addOption("--parse-metadata", "description:(?:Released on: )(?P\\d{4})") request.addOption("--parse-metadata", "%(dscrptn_year,release_year,release_date>%Y,upload_date>%Y)s:%(meta_date)s") @@ -1166,21 +1174,29 @@ class InfoUtil(private val context: Context) { } val cropThumb = downloadItem.audioPreferences.cropThumb ?: sharedPreferences.getBoolean("crop_thumbnail", true) - if (thumbnailFormat == "jpg"){ - request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1") - } - if (downloadItem.audioPreferences.embedThumb){ request.addOption("--embed-thumbnail") if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!) + + val thumbnailConfig = StringBuilder("") + val cropConfig = """-vf crop=\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\""""" + if (thumbnailFormat == "jpg") thumbnailConfig.append("""--ppa "ThumbnailsConvertor:-qmin 1 -q:v 1"""") if (cropThumb){ + if (thumbnailFormat == "jpg") { + thumbnailConfig.deleteCharAt(thumbnailConfig.length - 1) + thumbnailConfig.append(""" $cropConfig""") + } + else thumbnailConfig.append("""--ppa "ThumbnailsConvertor:$cropConfig""") + } + + if (thumbnailConfig.isNotBlank()){ runCatching { val config = File(context.cacheDir.absolutePath + "/config" + downloadItem.id + "##ffmpegCrop.txt") - val configData = "--ppa \"ThumbnailsConvertor:-vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\"" - config.writeText(configData) + config.writeText(thumbnailConfig.toString()) request.addOption("--config", config.absolutePath) } } + } if (filenameTemplate.isNotBlank()){ @@ -1220,19 +1236,20 @@ class InfoUtil(private val context: Context) { //format logic var videoF = downloadItem.format.format_id - var audioF = downloadItem.videoPreferences.audioFormatIDs.joinToString("+") { f -> + var altAudioF = "" + var audioF = downloadItem.videoPreferences.audioFormatIDs.map { f -> val format = downloadItem.allFormats.find { it.format_id == f } format?.run { if (this.format_id.contains("-")) { if (!this.lang.isNullOrBlank() && this.lang != "None") { "ba[format_id~='^(${this.format_id.split("-")[0]})'][language^=${this.lang}]" } else { - "${this.format_id}/${this.format_id.split("-")[0]}" + altAudioF = this.format_id.split("-")[0] + this.format_id } } else this.format_id } ?: f - - }.ifBlank { "ba" } + }.joinToString("+").ifBlank { "ba" } val preferredAudioLanguage = sharedPreferences.getString("audio_language", "")!! if (downloadItem.videoPreferences.removeAudio) audioF = "" @@ -1244,13 +1261,18 @@ class InfoUtil(private val context: Context) { val defaultFormats = context.resources.getStringArray(R.array.video_formats_values) val usingGenericFormat = defaultFormats.contains(videoF) || downloadItem.allFormats.isEmpty() || downloadItem.allFormats == getGenericVideoFormats(context.resources) + var hasGenericResulutionFormat = "" if (!usingGenericFormat){ // with audio removed if (audioF.isBlank()){ f.append("$videoF/bv/b") }else{ //with audio - f.append("$videoF+$audioF/$videoF+ba/$videoF/b") + f.append("$videoF+$audioF/") + if (altAudioF.isNotBlank()){ + f.append("$videoF+$altAudioF/") + } + f.append("$videoF+ba/$videoF/b") if (audioF.count("+") > 0){ request.addOption("--audio-multistreams") @@ -1263,7 +1285,8 @@ class InfoUtil(private val context: Context) { videoF = "wv" if (audioF == "ba") audioF = "wa" }else if (defaultFormats.contains(videoF)) { - videoF = "bv[height<="+videoF.split("_")[0].dropLast(1)+"]" + hasGenericResulutionFormat = videoF.split("_")[0].dropLast(1) + videoF = "bv" } val preferredFormatIDs = sharedPreferences.getString("format_id", "").toString() @@ -1331,6 +1354,7 @@ class InfoUtil(private val context: Context) { } StringBuilder().apply { + if (hasGenericResulutionFormat.isNotBlank()) append(",res:${hasGenericResulutionFormat}") if (sharedPreferences.getBoolean("prefer_smaller_formats", false)) append(",+size") if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref") if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref") @@ -1369,7 +1393,9 @@ class InfoUtil(private val context: Context) { if (downloadItem.videoPreferences.removeAudio){ - request.addOption("--ppa", "ffmpeg:-an") + request.addOption("--use-postprocessor", "FFmpegCopyStream") + request.addOption("--ppa", "CopyStream:-c copy -an") + } request.addOption("-P", downDir.absolutePath) @@ -1458,7 +1484,15 @@ class InfoUtil(private val context: Context) { val audioFormatsValues = resources.getStringArray(R.array.audio_formats_values) val formats = mutableListOf() val containerPreference = sharedPreferences.getString("audio_format", "") - val acodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac") + val acodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac")!!.run { + if (this.isEmpty()){ + resources.getString(R.string.defaultValue) + }else{ + val audioCodecs = resources.getStringArray(R.array.audio_codec) + val audioCodecsValues = resources.getStringArray(R.array.audio_codec_values) + audioCodecs[audioCodecsValues.indexOf(this)] + } + } audioFormats.forEachIndexed { idx, it -> formats.add(Format(audioFormatsValues[idx], containerPreference!!,"",acodecPreference!!, "",0, it)) } audioFormats.reverse() audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",resources.getString(R.string.preferred_format_id), "",1, it)) } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt index 1c627df5..6ccde65c 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt @@ -8,6 +8,7 @@ import android.app.TaskStackBuilder import android.content.Context import android.content.Intent import android.content.res.Resources +import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.os.Build @@ -243,19 +244,18 @@ class NotificationUtil(var context: Context) { } val bitmap = iconType.toBitmap(context) - val bigPictureStyle = NotificationCompat.BigPictureStyle() - bigPictureStyle.bigLargeIcon(bitmap) - if (Build.VERSION.SDK_INT >= 31){ - bigPictureStyle.showBigPictureWhenCollapsed(true) - } - notificationBuilder .setContentTitle("${res.getString(R.string.downloaded)} $title") .setSmallIcon(R.drawable.ic_launcher_foreground_large) + .setLargeIcon(bitmap) .setGroup(DOWNLOAD_FINISHED_NOTIFICATION_ID.toString()) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) .setContentText(title) - .setStyle(bigPictureStyle) + .setStyle(NotificationCompat.BigTextStyle() + .bigText(""" + $title + ${filepath?.joinToString("\n")} + """.trimIndent())) .setPriority(NotificationCompat.PRIORITY_MAX) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .clearActions() @@ -404,7 +404,7 @@ class NotificationUtil(var context: Context) { ) try { - notificationBuilder.setProgress(100, progress, progress == 0) + notificationBuilder.setProgress(100, progress, (progress == 0 || progress == 100)) .setContentTitle(title) .setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) .clearActions() diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt index bdce24ac..bbd04bdf 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -1,12 +1,18 @@ package com.deniscerri.ytdl.util +import android.animation.Animator +import android.animation.ObjectAnimator +import android.animation.ValueAnimator import android.annotation.SuppressLint import android.app.Activity import android.content.ClipData +import android.content.ClipData.Item import android.content.ClipboardManager import android.content.Context import android.content.DialogInterface import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.Canvas import android.net.Uri import android.os.Build import android.text.Editable @@ -26,10 +32,13 @@ import android.widget.EditText import android.widget.LinearLayout import android.widget.TextView import android.widget.Toast +import androidx.annotation.DimenRes import androidx.annotation.OptIn import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources +import androidx.cardview.widget.CardView +import androidx.compose.ui.graphics.Color import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.FileProvider import androidx.core.view.isVisible @@ -39,6 +48,7 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleOwner import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdl.R @@ -77,6 +87,8 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputLayout +import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE +import com.google.android.material.textfield.TextInputLayout.EndIconMode import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat import kotlinx.coroutines.CoroutineScope @@ -696,11 +708,11 @@ object UiUtil { true } } - DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused -> { - if (item.downloadStartTime <= System.currentTimeMillis() / 1000) download!!.visibility = View.GONE - else{ - download!!.text = context.getString(R.string.download_now) - } + DownloadRepository.Status.Queued -> { + download!!.visibility = View.GONE + } + DownloadRepository.Status.Scheduled -> { + download!!.text = context.getString(R.string.download_now) } else -> { download?.setOnLongClickListener { @@ -1803,9 +1815,10 @@ object UiUtil { } } - sheet.setOnDismissListener { + sheet.setOnDismissListener { _ -> CoroutineScope(Dispatchers.IO).launch { downloadViewModel.deleteProcessing() + downloadViewModel.deleteAllWithID(it.downloadItems) } } @@ -2023,7 +2036,10 @@ object UiUtil { builder.setTitle(context.getString(R.string.piped_instance)) val view = context.layoutInflater.inflate(R.layout.filename_template_dialog, null) val editText = view.findViewById(R.id.filename_edittext) - view.findViewById(R.id.filename).hint = context.getString(R.string.piped_instance) + view.findViewById(R.id.filename).apply { + hint = context.getString(R.string.piped_instance) + endIconMode = END_ICON_NONE + } editText.setText(currentInstance) editText.setSelection(editText.text.length) builder.setView(view) @@ -2142,4 +2158,28 @@ object UiUtil { val dialog = builder.create() dialog.show() } + + + fun getAlphaAnimator(view: View, alphaTo: Float): Animator { + return ObjectAnimator.ofFloat(view, View.ALPHA, view.alpha, alphaTo) + } + + fun getScaleXAnimator(view: View, scaleTo: Float): Animator { + return ObjectAnimator.ofFloat(view, View.SCALE_X, view.scaleX, scaleTo) + } + + fun getScaleYAnimator(view: View, scaleTo: Float): Animator { + return ObjectAnimator.ofFloat(view, View.SCALE_Y, view.scaleX, scaleTo) + } + + fun getElevationAnimator(view: MaterialCardView, @DimenRes elevationTo: Int): Animator { + val valueFrom = view.cardElevation + val valueTo = view.context.resources.getDimensionPixelSize(elevationTo).toFloat() + + return ValueAnimator.ofFloat(valueFrom, valueTo).apply { + addUpdateListener { + view.cardElevation = it.animatedValue as? Float ?: return@addUpdateListener + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt index 3a9168be..db7398da 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt @@ -64,10 +64,11 @@ class DownloadWorker( val alarmScheduler = AlarmScheduler(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val time = System.currentTimeMillis() + 6000 - val queuedItems = dao.getQueuedDownloadsThatAreNotScheduledChunked(time) + val queuedItems = dao.getQueuedScheduledDownloadsUntil(time) val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await() if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success() + // this is needed for observe sources call, so it wont create result items val createResultItem = inputData.getBoolean("createResultItem", true) val confTmp = Configuration(context.resources.configuration) @@ -176,21 +177,31 @@ class DownloadWorker( } val wasQuickDownloaded = resultDao.getCountInt() == 0 runBlocking { - var finalPaths : List? + var finalPaths : MutableList? if (noCache){ setProgressAsync(workDataOf("progress" to 100, "output" to "Scanning Files", "id" to downloadItem.id)) - finalPaths = it.out.split("\n") - .asSequence() + val outputSequence = it.out.split("\n") + finalPaths = + outputSequence.asSequence() .filter { it.startsWith("'/storage") } - .map { it.removePrefix("'") } - .map { it.removeSuffix("\n") } - .map { it.removeSuffix("'") } - .sortedBy { File(it).lastModified() } - .toList() + .map { it.removeSuffix("\n") } + .map { it.removeSurrounding("'", "'") } + .toMutableList() + + finalPaths.addAll( + outputSequence.asSequence() + .filter { it.startsWith("[SplitChapters]") && it.contains("Destination: ") } + .map { it.split("Destination: ")[1] } + .map { it.removeSuffix("\n") } + .toList() + ) + + finalPaths.sortBy { File(it).lastModified() } + finalPaths = finalPaths.distinct().toMutableList() FileUtil.scanMedia(finalPaths, context) if (finalPaths.isEmpty()){ - finalPaths = listOf(context.getString(R.string.unfound_file)) + finalPaths = mutableListOf(context.getString(R.string.unfound_file)) } }else{ //move file from internal to set download directory @@ -200,15 +211,15 @@ class DownloadWorker( FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p -> setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id)) } - }.filter { !it.matches("\\.(description)|(txt)\$".toRegex()) } + }.filter { !it.matches("\\.(description)|(txt)\$".toRegex()) }.toMutableList() if (finalPaths.isNotEmpty()){ setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id)) }else{ - finalPaths = listOf(context.getString(R.string.unfound_file)) + finalPaths = mutableListOf(context.getString(R.string.unfound_file)) } }catch (e: Exception){ - finalPaths = listOf(context.getString(R.string.unfound_file)) + finalPaths = mutableListOf(context.getString(R.string.unfound_file)) e.printStackTrace() handler.postDelayed({ Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() @@ -223,7 +234,7 @@ class DownloadWorker( add("description") add("txt") } - finalPaths = finalPaths?.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } } + finalPaths = finalPaths?.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } }?.toMutableList() FileUtil.deleteConfigFiles(request) //put download in history diff --git a/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt index e73f7b28..22fe9707 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt @@ -17,7 +17,7 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.HistoryRepository import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository import com.deniscerri.ytdl.database.repository.ResultRepository -import com.deniscerri.ytdl.util.Extensions.calculateNextTime +import com.deniscerri.ytdl.util.Extensions.calculateNextTimeForObserving import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.NotificationUtil @@ -210,7 +210,7 @@ class ObserveSourceWorker( .addTag("observeSources") .addTag(sourceID.toString()) .setConstraints(workConstraints.build()) - .setInitialDelay(System.currentTimeMillis() - item.calculateNextTime(), TimeUnit.MILLISECONDS) + .setInitialDelay(item.calculateNextTimeForObserving() - System.currentTimeMillis(), TimeUnit.MILLISECONDS) .setInputData(Data.Builder().putLong("id", sourceID).build()) WorkManager.getInstance(context).enqueueUniqueWork( diff --git a/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt index 1b4f939a..741ad36b 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt @@ -25,8 +25,10 @@ import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDLRequest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import java.io.File @@ -140,29 +142,23 @@ class TerminalDownloadWorker( } } - return runBlocking { - if (logDownloads) logRepo.update(it.out, logItem.id) - dao.updateLog(it.out, itemId.toLong()) - dao.delete(itemId.toLong()) - notificationUtil.cancelDownloadNotification(itemId) - - Result.success() - } + if (logDownloads) logRepo.update(it.out, logItem.id) + dao.updateLog(it.out, itemId.toLong()) + notificationUtil.cancelDownloadNotification(itemId) + delay(1000) + dao.delete(itemId.toLong()) + Result.success() }.onFailure { - return runBlocking { - CoroutineScope(Dispatchers.IO).launch { - if (it.message != null){ - if (logDownloads) logRepo.update(it.message!!, logItem.id) - dao.updateLog(it.message!!, itemId.toLong()) - dao.delete(itemId.toLong()) - } - } - notificationUtil.cancelDownloadNotification(itemId) - File(FileUtil.getDefaultCommandPath() + "/" + itemId).deleteRecursively() - Log.e(TAG, context.getString(R.string.failed_download), it) - Result.failure() + if (it.message != null){ + if (logDownloads) logRepo.update(it.message!!, logItem.id) + dao.updateLog(it.message!!, itemId.toLong()) } - + notificationUtil.cancelDownloadNotification(itemId) + File(FileUtil.getDefaultCommandPath() + "/" + itemId).deleteRecursively() + Log.e(TAG, context.getString(R.string.failed_download), it) + delay(1000) + dao.delete(itemId.toLong()) + Result.failure() } diff --git a/app/src/main/res/drawable/baseline_downloading_red.xml b/app/src/main/res/drawable/baseline_downloading_red.xml new file mode 100644 index 00000000..fe7189e5 --- /dev/null +++ b/app/src/main/res/drawable/baseline_downloading_red.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_filter_alt_24.xml b/app/src/main/res/drawable/baseline_filter_alt_24.xml new file mode 100644 index 00000000..7898950d --- /dev/null +++ b/app/src/main/res/drawable/baseline_filter_alt_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_layers_24.xml b/app/src/main/res/drawable/baseline_layers_24.xml new file mode 100644 index 00000000..8c79cd68 --- /dev/null +++ b/app/src/main/res/drawable/baseline_layers_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_more_vert_24.xml b/app/src/main/res/drawable/baseline_more_vert_24.xml new file mode 100644 index 00000000..0302ac25 --- /dev/null +++ b/app/src/main/res/drawable/baseline_more_vert_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_downloads_red.xml b/app/src/main/res/drawable/ic_downloads_red.xml new file mode 100644 index 00000000..5c7f2490 --- /dev/null +++ b/app/src/main/res/drawable/ic_downloads_red.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_drag_handle.xml b/app/src/main/res/drawable/ic_drag_handle.xml new file mode 100644 index 00000000..b89447b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_handle.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_search_red.xml b/app/src/main/res/drawable/ic_search_red.xml new file mode 100644 index 00000000..953e2d6c --- /dev/null +++ b/app/src/main/res/drawable/ic_search_red.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_terminal_formatcard.xml b/app/src/main/res/drawable/ic_terminal_formatcard.xml new file mode 100644 index 00000000..f16cabde --- /dev/null +++ b/app/src/main/res/drawable/ic_terminal_formatcard.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_terminal_red.xml b/app/src/main/res/drawable/ic_terminal_red.xml new file mode 100644 index 00000000..790aca2a --- /dev/null +++ b/app/src/main/res/drawable/ic_terminal_red.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_video_formatcard.xml b/app/src/main/res/drawable/ic_video_formatcard.xml new file mode 100644 index 00000000..a8eaea08 --- /dev/null +++ b/app/src/main/res/drawable/ic_video_formatcard.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/active_download_card.xml b/app/src/main/res/layout/active_download_card.xml index 24b56f59..ef58d8e1 100644 --- a/app/src/main/res/layout/active_download_card.xml +++ b/app/src/main/res/layout/active_download_card.xml @@ -166,23 +166,6 @@ app:layout_constraintVertical_bias="0.51" > - - - + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + + + android:layout_height="80dp"> - - - - - - - - - + app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" /> + app:layout_constraintEnd_toStartOf="@+id/options" + app:layout_constraintStart_toEndOf="@+id/downloads_image_view" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.0"> - + android:layout_height="wrap_content"> + + + + + + + + + + + + + + - - - - - - + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/baseline_more_vert_24" + app:tint="?attr/colorControlNormal" + tools:ignore="ContentDescription" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/download_card.xml b/app/src/main/res/layout/download_card.xml index c112bf75..63b95761 100644 --- a/app/src/main/res/layout/download_card.xml +++ b/app/src/main/res/layout/download_card.xml @@ -109,6 +109,44 @@ android:layout_width="wrap_content" android:layout_height="match_parent"> + + + + - + diff --git a/app/src/main/res/layout/fragment_active.xml b/app/src/main/res/layout/fragment_active.xml index 8ca84871..b94db64a 100644 --- a/app/src/main/res/layout/fragment_active.xml +++ b/app/src/main/res/layout/fragment_active.xml @@ -9,18 +9,6 @@ android:layout_height="match_parent" android:orientation="vertical"> - - + + + + + + + android:visibility="gone" + android:layout_width="match_parent"/> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_inqueue.xml b/app/src/main/res/layout/fragment_inqueue.xml index 533c551c..54a3a860 100644 --- a/app/src/main/res/layout/fragment_inqueue.xml +++ b/app/src/main/res/layout/fragment_inqueue.xml @@ -11,19 +11,36 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + + + + + + + diff --git a/app/src/main/res/layout/queued_download_card.xml b/app/src/main/res/layout/queued_download_card.xml new file mode 100644 index 00000000..bb90febf --- /dev/null +++ b/app/src/main/res/layout/queued_download_card.xml @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/scheduled_download_card.xml b/app/src/main/res/layout/scheduled_download_card.xml new file mode 100644 index 00000000..b6cab1cf --- /dev/null +++ b/app/src/main/res/layout/scheduled_download_card.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/layout/template_shortcuts.xml b/app/src/main/res/layout/template_shortcuts.xml index e48ca4a0..b2b89264 100644 --- a/app/src/main/res/layout/template_shortcuts.xml +++ b/app/src/main/res/layout/template_shortcuts.xml @@ -66,6 +66,7 @@ diff --git a/app/src/main/res/menu/active_downloads_minified.xml b/app/src/main/res/menu/active_downloads_minified.xml new file mode 100644 index 00000000..bf150d99 --- /dev/null +++ b/app/src/main/res/menu/active_downloads_minified.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/menu/download_queue_menu.xml b/app/src/main/res/menu/download_queue_menu.xml index 111e0e76..d31d5835 100644 --- a/app/src/main/res/menu/download_queue_menu.xml +++ b/app/src/main/res/menu/download_queue_menu.xml @@ -15,6 +15,12 @@ android:title="@string/clear_cancelled" app:showAsAction="never" /> + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/queued_menu_context.xml b/app/src/main/res/menu/queued_menu_context.xml index 0cb8e797..31ba86ac 100644 --- a/app/src/main/res/menu/queued_menu_context.xml +++ b/app/src/main/res/menu/queued_menu_context.xml @@ -20,7 +20,13 @@ android:id="@+id/up" android:icon="@drawable/ic_arrow_up" app:showAsAction="ifRoom" - android:title="Move Up" /> + android:title="@string/move_top" /> + + شهر معدّل البتات غير المحذوفة - استخدام ملف أرشيف التنزيل الخاص بـ yt-dlp لمنع التنزيل المكرر بدلًا من الطريقة الافتراضية - أرشيف التنزيل + أرشيف التنزيل [استخدام ملف أرشيف التنزيل الخاص بـ yt-dlp لمنع التنزيل المكرر بدلًا من الطريقة الافتراضية] في إعادة المحاولة للتنزيلات المفقودة حفظ الترجمات التلقائية diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 27604b6a..aa83a015 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -351,14 +351,13 @@ Bit Sürəti Sonra Silinməyib - Yükləmə Arxivi + Yükləmə Arxivi [Dublikat yükləmələrə mane olmaq üçün daxili funksionallıq əvəzinə, yt-dlp-nin yükləmə arxiv faylın istifadə et] Mənbələrə Nəzarət Et Bütün mənbələri həmişəlik sil Başlayır proses Avtomatik Titirləri Saxla - Dublikat yükləmələrə mane olmaq üçün daxili funksionallıq əvəzinə, yt-dlp-nin yükləmə arxiv faylın istifadə et Mətn Ölçüsü Əlavə et Yükləmə Kartında Sürüşdürmə Jestləri diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index f1e9052c..fcee4c55 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -350,7 +350,7 @@ Opakovat zmeškané stahování Uložit automatické titulky Neodstraněno - Stáhnout archiv + Stáhnout archiv [Použije archivní soubor yt-dlp pro zabránění duplicitního stahování místo vestavěné funkce] Sledovat zdroje Nový zfroj Trvale odstraní všechny zdroje @@ -358,7 +358,6 @@ Zapnout Měsíc Rychlost přenosu - Použije archivní soubor yt-dlp pro zabránění duplicitního stahování místo vestavěné funkce Zobrazení počtů na obrazovce stahovací fronty Formát miniatur Získat pouze nové nahrávky diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ab014ad8..9d8bc585 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -353,8 +353,7 @@ Tasa de datos Guardar subtítulos automáticos No eliminado - Descargar archivo - Utilice el archivo de descarga de yt-dlp para evitar descargas duplicadas en lugar de la funcionalidad integrada + Descargar archivo [Utilice el archivo de descarga de yt-dlp para evitar descargas duplicadas en lugar de la funcionalidad integrada] En Semana Borrar todas las fuentes, permanentemente diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 5848b129..241a2bad 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -350,14 +350,13 @@ बिटरेट स्वचालित उपशीर्षक सहेजें हटाया नहीं गया - डाउनलोड संग्रह + डाउनलोड संग्रह [डुप्लिकेट डाउनलोड को रोकने के लिए अंतर्निहित कार्यक्षमता के बजाय yt-dlp की डाउनलोड संग्रह फ़ाइल का उपयोग करें] नियत समय पर सभी स्रोत हटाएँ IPv4 को बाध्य करें कभी नहीं URL कॉपी करें घटनाएँ - डुप्लिकेट डाउनलोड को रोकने के लिए अंतर्निहित कार्यक्षमता के बजाय yt-dlp की डाउनलोड संग्रह फ़ाइल का उपयोग करें गुम हुए डाउनलोड पुनः प्रयास करें थंबनेल प्रारूप केवल नए अपलोड प्राप्त करें diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index a3567c44..8583af21 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -352,8 +352,7 @@ Bitráta Feliratok automatikus mentése Nincs törölve - Archívum letöltése - Használja az yt-dlp letöltési archívum fájlját a duplikált letöltések megakadályozására a beépített funkció helyett + Archívum letöltése [Használja az yt-dlp letöltési archívum fájlját a duplikált letöltések megakadályozására a beépített funkció helyett] Be események Nap diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index fd918aa2..38d1d163 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -342,10 +342,9 @@ Coba Kembali Unduhan yang Hilang Laju Bit Simpan Takarir (Subtitle) Otomatis - Arsip Unduhan + Arsip Unduhan [Gunakan berkas arsip unduhan yt-dlp untuk mencegah unduhan duplikat alih-alih menggunakan fungsionalitas bawaan] Amati Sumber Hapus semua sumber, secara permanen - Gunakan berkas arsip unduhan yt-dlp untuk mencegah unduhan duplikat alih-alih menggunakan fungsionalitas bawaan Hari Tidak Dihapus Log Perubahan diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f7072024..0e4587a2 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -351,7 +351,7 @@ Velocità di trasmissione Salva i Sottotitoli Automatici Non Eliminato - Scarica Archivio + Scarica Archivio [Utilizza l\'archivio download di yt-dlp per evitare download duplicati invece della funzionalità integrata] Acceso Non scaricare come Frammenti Non usare i file .part e scrivi direttamente nel file di output @@ -366,7 +366,6 @@ Cancella tutti i sorgenti, permanentemente Copia URLs Mai - Utilizza l\'archivio download di yt-dlp per evitare download duplicati invece della funzionalità integrata Gestualità di Scorrimento nella download card Ricevi Solo Nuovi Upload Scarica Anche come Audio diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 60efcd8c..9cc81594 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -345,9 +345,8 @@ ダウンロード失敗時に再試行 未削除 - ダウンロード記録 + ダウンロード記録 [内蔵の機能ではなく yt-dlp のダウンロード記録ファイルを使い重複ダウンロードを防止] 自動字幕を保存 - 内蔵の機能ではなく yt-dlp のダウンロード記録ファイルを使い重複ダウンロードを防止 すべての接続を IPv4 で行う 指定先を監視 新しい監視先 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 4b7dabfb..6c0875b2 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -337,8 +337,7 @@ 누락된 다운로드 재시도 자동 자막 저장 삭제되지 않음 - 아카이브 다운로드 - 내장된 기능 대신 yt-dlp의 다운로드 아카이브를 사용하여 중복된 다운로드를 방지합니다 + 아카이브 다운로드 [내장된 기능 대신 yt-dlp의 다운로드 아카이브를 사용하여 중복된 다운로드를 방지합니다] 선호하는 오디오 언어 모든 항목의 다운로드 경로를 바꾸기: 무시 diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 284d5067..3390dd44 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -349,7 +349,7 @@ ਗੁੰਮ ਹੋਏ ਡਾਉਨਲੋਡਸ ਲਈ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਬਿੱਟਰੇਟ ਮਿਟਾਇਆ ਨਹੀਂ ਗਿਆ - ਡਾਊਨਲੋਡ ਆਰਕਾਈਵ + ਡਾਊਨਲੋਡ ਆਰਕਾਈਵ [ਬਿਲਟ-ਇਨ ਫੰਕਸ਼ਨੈਲਿਟੀ ਦੀ ਬਜਾਏ ਡੁਪਲੀਕੇਟ ਡਾਉਨਲੋਡਸ ਨੂੰ ਰੋਕਣ ਲਈ yt-dlp ਦੀ ਡਾਉਨਲੋਡ ਆਰਕਾਈਵ ਫਾਈਲ ਦੀ ਵਰਤੋਂ ਕਰੋ] \'ਤੇ ਸਰੋਤਾਂ ਦੀ ਨਿਗਰਾਨੀ ਕਰੋ ਸਾਰੇ ਸਰੋਤਾਂ ਨੂੰ ਪੱਕੇ ਤੌਰ \'ਤੇ ਮਿਟਾਓ @@ -358,7 +358,6 @@ ਕਦੇ ਨਹੀਂ ਖਤਮ ਹੁੰਦਾ ਹੈ ਸਵੈਚਲਿਤ ਸਬਟਾਈਟਲਾਂ ਨੂੰ ਸੇਵ ਕਰੋ - ਬਿਲਟ-ਇਨ ਫੰਕਸ਼ਨੈਲਿਟੀ ਦੀ ਬਜਾਏ ਡੁਪਲੀਕੇਟ ਡਾਉਨਲੋਡਸ ਨੂੰ ਰੋਕਣ ਲਈ yt-dlp ਦੀ ਡਾਉਨਲੋਡ ਆਰਕਾਈਵ ਫਾਈਲ ਦੀ ਵਰਤੋਂ ਕਰੋ ਡਾਉਨਲੋਡ ਕਾਰਡ ਵਿੱਚ ਸਵਾਈਪ ਇਸ਼ਾਰੇ ਥੰਮਨੇਲ ਫਾਰਮੈਟ ਟੈਕਸਟ ਦਾ ਆਕਾਰ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 3d98b184..76a49642 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -354,8 +354,7 @@ Ustaw czas Uruchom Spróbuj pobrać ponownie - Pobierz archiwum - Użyj pliku archiwum pobierania yt-dlp, aby zapobiec duplikatom pobierania zamiast wbudowanej funkcjonalności + Pobierz archiwum [Użyj pliku archiwum pobierania yt-dlp, aby zapobiec duplikatom pobierania zamiast wbudowanej funkcjonalności] Miesiąc Nie usunięte Zapisz automatyczne napisy diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 05f4c893..cc3bb5b5 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -347,7 +347,7 @@ Mês(es) Taxa de bits Não excluído - Arquivar downloads + Arquivar downloads [Usar o arquivamento de downloads do yt-dlp para evitar downloads duplicados] Observar Origens Excluir todas as origens Excluir todas as origens permanentemente @@ -356,7 +356,6 @@ ocorrências Dia(s) Salvar legendas automáticas - Usar o arquivamento de downloads do yt-dlp para evitar downloads duplicados Repetir downloads ausentes Efetuar todas as conexões com IPv4 Formato da miniatura diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 4df6ac7d..2e3dfe3d 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -357,8 +357,7 @@ Mês Definir tempo Em - Arquivo de descarga - Utilizar ficheiro de descarga yt-dlp para evitar duplicação de descargas + Arquivo de descarga [Utilizar ficheiro de descarga yt-dlp para evitar duplicação de descargas] Gestos de deslizar no cartão de descarga Mostrar contagens no ecrã da Fila de descarga Obter apenas novos envios diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bdbecc68..ae71c3c7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -346,7 +346,6 @@ Вкл День случаев - Используйте архивный файл загрузок yt-dlp, чтобы предотвратить дублирование загрузок вместо встроенной функции Источники загрузки Новый источник Удалить все источники навсегда @@ -358,7 +357,7 @@ Битрейт Сохранить автоматически субтитры Не удалено - Скачать архив + Скачать архив [Используйте архивный файл загрузок yt-dlp, чтобы предотвратить дублирование загрузок вместо встроенной функции] Жесты смахивания в карточке загрузок Показывать кол-во контента на экране очереди загрузки Формат миниатюр diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 99b06094..211a1282 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -345,8 +345,7 @@ Opakovať zmeškané sťahovania Uložiť automatické titulky Nevymazané - Stiahnuť archív - Použije archívny súbor yt-dlp namiesto vstavanej funkcie, aby sa zabránilo duplicitnému sťahovaniu + Stiahnuť archív [Použije archívny súbor yt-dlp namiesto vstavanej funkcie, aby sa zabránilo duplicitnému sťahovaniu] Kopírovať adresy URL Sledovať zdroje Nový zdroj diff --git a/app/src/main/res/values-sq-rAL/strings.xml b/app/src/main/res/values-sq-rAL/strings.xml index ea63e9da..52917c8a 100644 --- a/app/src/main/res/values-sq-rAL/strings.xml +++ b/app/src/main/res/values-sq-rAL/strings.xml @@ -357,8 +357,7 @@ Mbaron Ditë Muaj - Arkiva e Shkarkimeve - Përdor skedarin e arkivës të yt-dlp për të parandaluar shkarkimet duplicate në vend të funksionalitetit të aplikacionit + Arkiva e Shkarkimeve [Përdor skedarin e arkivës të yt-dlp për të parandaluar shkarkimet duplicate] Gjestet e rrëshqitjes në kartën e shkarkimit Shfaq numrat në faqen e rradhës së shkarkimeve Shkarko Gjithashtu si Audio diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index c10fe203..f0648c88 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -357,8 +357,7 @@ Сваки Након Није избрисано - Користите yt-dlp архиву преузимања да бисте спречили дупла преузимања, уместо уграђене функционалности - Архива преузимања + Архива преузимања [Користите yt-dlp архиву преузимања да бисте спречили дупла преузимања, уместо уграђене функционалности] Прикажи бројеве на екрану редоследа за преузимање Добијај само нова отпремања Такође преузми као аудио снимак diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml index 8c327ba5..f6a18b45 100644 --- a/app/src/main/res/values-tr-rTR/strings.xml +++ b/app/src/main/res/values-tr-rTR/strings.xml @@ -356,12 +356,11 @@ Bit hızı Otomatik Altyazıları Kaydet Silinmedi - Yerleşik işlevsellik yerine yinelenen indirmeleri önlemek için yt-dlp\'nin indirme arşiv dosyasını kullanın Herhangi Asla Çalışmakta Gün - İndirme Arşivi + İndirme Arşivi [Yerleşik işlevsellik yerine yinelenen indirmeleri önlemek için yt-dlp\'nin indirme arşiv dosyasını kullanın] Küçük Resim Formatı Yalnızca Yeni Yüklemeleri Alın Ayrıca Ses Olarak İndir diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 91989a69..26daa484 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -357,8 +357,7 @@ Місяць Не видалено Зберегти автоматично субтитри - Архів завантажень - Використовуйте файл архіву завантажень yt-dlp, щоб запобігти повторним завантаженням замість вбудованої функції + Архів завантажень [Використовуйте файл архіву завантажень yt-dlp, щоб запобігти повторним завантаженням замість вбудованої функції] Жести пальцем у картці завантаження Показати підрахунки на екрані черги завантаження Формат мініатюр diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 8462d0c3..a462569c 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -336,8 +336,7 @@ Thực hiện tất cả các kết nối qua IPv4 Sao chép URL Lưu phụ đề tự động - Sử dụng tải xuống tệp lưu trữ của yt-dlp để ngăn tải xuống trùng lặp thay vì chức năng tích hợp sẵn - Tải xuống lưu trữ + Tải xuống lưu trữ [Sử dụng tải xuống tệp lưu trữ của yt-dlp để ngăn tải xuống trùng lặp thay vì chức năng tích hợp sẵn] Bỏ qua Đã thay đổi đường dẫn tải xuống của mọi mục thành: Tất cả diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c156d92e..acde6d86 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -352,13 +352,12 @@ 结束 - 使用 yt-dlp 的下载存档文件而非应用的内置功能来防止重复下载 重试缺少的下载 未删除 比特率 保存自动字幕 - 下载存档 + 下载存档 [使用 yt-dlp 的下载存档文件而非应用的内置功能来防止重复下载] 同样下载为音频 换行显示过长文本 文本尺寸 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 47598fdf..9a8cfd2a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -348,7 +348,7 @@ 從不 之後 次數 - 下載存檔 + 下載存檔 [使用 yt-dlp 的下載存檔檔案以防止重複下載,而非使用內建功能] 未刪除 儲存自動字幕 @@ -356,7 +356,6 @@ 重試遺漏的下載 比特率 - 使用 yt-dlp 的下載存檔檔案以防止重複下載,而非使用內建功能 使所有連線均使用 IPv4 \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index a38840dc..191650da 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -25,14 +25,18 @@ @string/defaultValue mp4 mkv - webm + avi + flv + mov Default mp4 mkv - webm + avi + flv + mov @@ -116,6 +120,7 @@ Azərbaycanca বাংলা বাংলা (India) + български Босански Deutsch Ἑλληνική @@ -159,6 +164,7 @@ az bn bn-IN + bg bs de el @@ -1084,6 +1090,7 @@ @string/downloads @string/in_queue @string/cancelled + @string/scheduled @string/errored @string/saved @string/command_templates @@ -1095,6 +1102,7 @@ history queued cancelled + scheduled errored saved templates @@ -1396,4 +1404,18 @@ jpg + + @string/disabled + @string/archive + @string/prevent_duplicate_url_type + @string/prevent_duplicate_config + + + + + download_archive + url_type + config + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a6b3daec..945a0139 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,2 +1,5 @@ - \ No newline at end of file + + 6dp + 2dp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ffd2ee97..7042194c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -360,8 +360,8 @@ Bitrate Save Automatic Subtitles Not Deleted - Download Archive - Use yt-dlp\'s download archive file to prevent duplicate downloads instead of the built in functionality + Download Archive [Use yt-dlp\'s download archive file to prevent duplicate downloads instead of the built in functionality] + Disabled Swipe Gestures in download card Show counts in the download queue screen Thumbnail Format @@ -382,4 +382,13 @@ Display Over Apps Make the download card always display on top. Turn this on if the currently running app closes when the download card shows up Hour + Subtitles + Prevent Duplicate Downloads + + Full Configuration + Copy URL + Move to the Top + Move to the Bottom + Scheduled + Clear Scheduled \ No newline at end of file diff --git a/app/src/main/res/xml-v25/shortcuts.xml b/app/src/main/res/xml-v25/shortcuts.xml index 29ef6be4..f514e2f3 100644 --- a/app/src/main/res/xml-v25/shortcuts.xml +++ b/app/src/main/res/xml-v25/shortcuts.xml @@ -2,7 +2,7 @@