This commit is contained in:
zaednasr 2024-04-14 22:53:16 +02:00
parent d7819aca98
commit 78efd276d8
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
99 changed files with 2665 additions and 793 deletions

View file

@ -10,7 +10,7 @@ plugins {
def properties = new Properties() def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 7 def versionMinor = 7
def versionPatch = 4 def versionPatch = 5
def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def versionExt = "" def versionExt = ""
@ -144,7 +144,7 @@ dependencies {
implementation "androidx.navigation:navigation-ui-ktx:$navVer" implementation "androidx.navigation:navigation-ui-ktx:$navVer"
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.test.ext:junit-ktx:1.1.5' 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" testImplementation "junit:junit:$junitVer"
androidTestImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer"
androidTestImplementation "androidx.test.ext:junit:$androidJunitVer" androidTestImplementation "androidx.test.ext:junit:$androidJunitVer"
@ -196,6 +196,6 @@ dependencies {
implementation "com.anggrayudi:storage:1.5.5" implementation "com.anggrayudi:storage:1.5.5"
implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'me.zhanghai.android.fastscroll:library:1.3.0'
implementation 'com.google.accompanist:accompanist-webview:0.30.1' 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" implementation "io.noties.markwon:core:4.6.2"
} }

View file

@ -20,7 +20,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads ORDER BY status") @Query("SELECT * FROM downloads ORDER BY status")
fun getAllDownloads() : PagingSource<Int, DownloadItem> fun getAllDownloads() : PagingSource<Int, DownloadItem>
@Query("SELECT * FROM downloads WHERE status in ('Active', 'ActivePaused', 'PausedReQueued')") @Query("SELECT * FROM downloads WHERE status in ('Active', 'ActivePaused')")
fun getActiveDownloads() : Flow<List<DownloadItem>> fun getActiveDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status = 'Processing'") @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") @Query("SELECT * FROM downloads WHERE status = 'Processing' ORDER BY id LIMIT 1")
fun getFirstProcessingDownload() : DownloadItem 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<DownloadItem> fun getActiveAndPausedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status = 'Processing'") @Query("SELECT * FROM downloads WHERE status = 'Processing'")
@ -61,32 +61,32 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Active'") @Query("SELECT * FROM downloads WHERE status='Active'")
suspend fun getActiveDownloadsList() : List<DownloadItem> suspend fun getActiveDownloadsList() : List<DownloadItem>
@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<DownloadItem> fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@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<Long> fun getActiveAndQueuedDownloadIDs() : List<Long>
@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<List<DownloadItem>> fun getActiveAndQueuedDownloads() : Flow<List<DownloadItem>>
@RewriteQueriesToDropUnusedColumns @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<Int, DownloadItemSimple> fun getQueuedDownloads() : PagingSource<Int, DownloadItemSimple>
@Query("SELECT format FROM downloads WHERE status in('Queued','QueuedPaused')") @Query("SELECT format FROM downloads WHERE status in('Queued','QueuedPaused')")
fun getSelectedFormatFromQueued() : List<Format> fun getSelectedFormatFromQueued() : List<Format>
@Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status in ('Queued', 'PausedReQueued') ORDER BY downloadStartTime, id LIMIT 20") @Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status in ('Queued', 'Scheduled') ORDER BY downloadStartTime, id LIMIT 20")
fun getQueuedDownloadsThatAreNotScheduledChunked(currentTime: Long) : Flow<List<DownloadItem>> fun getQueuedScheduledDownloadsUntil(currentTime: Long) : Flow<List<DownloadItem>>
@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<List<DownloadItem>> fun getQueuedAndPausedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id") @Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id")
fun getQueuedDownloadsList() : List<DownloadItem> fun getQueuedDownloadsList() : List<DownloadItem>
@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<Long> fun getQueuedDownloadsListIDs() : List<Long>
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
@ -98,7 +98,6 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'") @Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'")
fun getPausedDownloadsList() : List<DownloadItem> fun getPausedDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloads() : PagingSource<Int, DownloadItemSimple> fun getErroredDownloads() : PagingSource<Int, DownloadItemSimple>
@ -106,6 +105,10 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloadsList() : List<DownloadItem> fun getErroredDownloadsList() : List<DownloadItem>
@Query("SELECT id from downloads WHERE status='Scheduled' ORDER BY downloadStartTime, id DESC")
fun getScheduledDownloadIDs(): List<Long>
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC")
fun getSavedDownloads() : PagingSource<Int, DownloadItemSimple> fun getSavedDownloads() : PagingSource<Int, DownloadItemSimple>
@ -113,6 +116,10 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC")
fun getSavedDownloadsList() : List<DownloadItem> fun getSavedDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Scheduled' ORDER BY downloadStartTime, id DESC")
fun getScheduledDownloads() : PagingSource<Int, DownloadItemSimple>
@Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1")
fun getDownloadById(id: Long) : DownloadItem fun getDownloadById(id: Long) : DownloadItem
@ -165,10 +172,13 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE status='Processing'") @Query("DELETE FROM downloads WHERE status='Processing'")
suspend fun deleteProcessing() suspend fun deleteProcessing()
@Query("DELETE FROM downloads WHERE status='Scheduled'")
suspend fun deleteScheduled()
@Query("DELETE FROM downloads WHERE id in (:list)") @Query("DELETE FROM downloads WHERE id in (:list)")
suspend fun deleteAllWithIDs(list: List<Long>) suspend fun deleteAllWithIDs(list: List<Long>)
@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() suspend fun cancelActiveQueued()
@Query("DELETE FROM downloads WHERE status='Processing' AND id=:id") @Query("DELETE FROM downloads WHERE status='Processing' AND id=:id")
@ -244,6 +254,22 @@ interface DownloadDao {
} }
} }
@Transaction
suspend fun putAtBottomOfTheQueue(existingIDs: List<Long>){
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") @Query("Update downloads set id=:newId where id=:id")
suspend fun updateDownloadID(id: Long, newId: Long) 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") @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<String>) : List<Long> fun getIDsBetweenTwoItems(item1: Long, item2: Long, statuses: List<String>) : List<Long>
@Query("SELECT id from downloads WHERE id > :item1 AND id < :item2 AND status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id") @Query("SELECT id from downloads WHERE id > :item1 AND id < :item2 AND status in('Scheduled') ORDER BY downloadStartTime, id")
fun getQueuedIDsBetweenTwoItems(item1: Long, item2: Long) : List<Long> fun getScheduledIDsBetweenTwoItems(item1: Long, item2: Long) : List<Long>
} }

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdl.database.models
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
@Entity(tableName = "downloads") @Entity(tableName = "downloads")
data class DownloadItemSimple( data class DownloadItemSimple(
@ -16,5 +17,8 @@ data class DownloadItemSimple(
var format: Format, var format: Format,
@ColumnInfo(defaultValue = "Queued") @ColumnInfo(defaultValue = "Queued")
var status: String, var status: String,
var logID: Long? var logID: Long?,
var type: DownloadViewModel.Type,
@ColumnInfo(defaultValue = "0")
var downloadStartTime: Long,
) )

View file

@ -53,15 +53,23 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
pagingSourceFactory = {downloadDao.getSavedDownloads()} pagingSourceFactory = {downloadDao.getSavedDownloads()}
) )
val scheduledDownloads: Pager<Int, DownloadItemSimple> = Pager(
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
pagingSourceFactory = {downloadDao.getScheduledDownloads()}
)
val activeDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.ActivePaused, Status.PausedReQueued).toListString()) val activeDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active).toListString())
val activeAndActivePausedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.ActivePaused).toListString())
val queuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Queued, Status.QueuedPaused).toListString()) val queuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Queued, Status.QueuedPaused).toListString())
val activeQueuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.ActivePaused, Status.Queued, Status.QueuedPaused).toListString())
val cancelledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Cancelled).toListString()) val cancelledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Cancelled).toListString())
val erroredDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Error).toListString()) val erroredDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Error).toListString())
val savedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Saved).toListString()) val savedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Saved).toListString())
val pausedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.ActivePaused, Status.QueuedPaused).toListString())
val scheduledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Scheduled).toListString())
enum class Status { 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 { suspend fun insert(item: DownloadItem) : Long {
@ -139,12 +147,20 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getErroredDownloadsList() return downloadDao.getErroredDownloadsList()
} }
fun getScheduledDownloadIDs() : List<Long> {
return downloadDao.getScheduledDownloadIDs()
}
suspend fun deleteCancelled(){ suspend fun deleteCancelled(){
val cancelled = getCancelledDownloads() val cancelled = getCancelledDownloads()
downloadDao.deleteCancelled() downloadDao.deleteCancelled()
deleteCache(cancelled) deleteCache(cancelled)
} }
suspend fun deleteScheduled() {
downloadDao.deleteScheduled()
}
suspend fun deleteErrored(){ suspend fun deleteErrored(){
val errored = getErroredDownloads() val errored = getErroredDownloads()
downloadDao.deleteErrored() downloadDao.deleteErrored()

View file

@ -35,6 +35,7 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ResultRepository import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.ui.downloadcard.FormatTuple 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.FileUtil
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.AlarmScheduler
@ -65,12 +66,17 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val cancelledDownloads : Flow<PagingData<DownloadItemSimple>> val cancelledDownloads : Flow<PagingData<DownloadItemSimple>>
val erroredDownloads : Flow<PagingData<DownloadItemSimple>> val erroredDownloads : Flow<PagingData<DownloadItemSimple>>
val savedDownloads : Flow<PagingData<DownloadItemSimple>> val savedDownloads : Flow<PagingData<DownloadItemSimple>>
val scheduledDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloadsCount : Flow<Int> val activeDownloadsCount : Flow<Int>
val activeAndActivePausedDownloadsCount : Flow<Int>
val queuedDownloadsCount : Flow<Int> val queuedDownloadsCount : Flow<Int>
val activeQueuedDownloadsCount : Flow<Int>
val cancelledDownloadsCount : Flow<Int> val cancelledDownloadsCount : Flow<Int>
val erroredDownloadsCount : Flow<Int> val erroredDownloadsCount : Flow<Int>
val savedDownloadsCount : Flow<Int> val savedDownloadsCount : Flow<Int>
val scheduledDownloadsCount : Flow<Int>
val pausedDownloadsCount: Flow<Int>
private var bestVideoFormat : Format private var bestVideoFormat : Format
private var bestAudioFormat : Format private var bestAudioFormat : Format
@ -122,16 +128,21 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
infoUtil = InfoUtil(application) infoUtil = InfoUtil(application)
activeDownloadsCount = repository.activeDownloadsCount activeDownloadsCount = repository.activeDownloadsCount
activeAndActivePausedDownloadsCount = repository.activeAndActivePausedDownloadsCount
queuedDownloadsCount = repository.queuedDownloadsCount queuedDownloadsCount = repository.queuedDownloadsCount
activeQueuedDownloadsCount = repository.activeQueuedDownloadsCount
cancelledDownloadsCount = repository.cancelledDownloadsCount cancelledDownloadsCount = repository.cancelledDownloadsCount
erroredDownloadsCount = repository.erroredDownloadsCount erroredDownloadsCount = repository.erroredDownloadsCount
savedDownloadsCount = repository.savedDownloadsCount savedDownloadsCount = repository.savedDownloadsCount
scheduledDownloadsCount = repository.scheduledDownloadsCount
pausedDownloadsCount = repository.pausedDownloadsCount
allDownloads = repository.allDownloads.flow allDownloads = repository.allDownloads.flow
queuedDownloads = repository.queuedDownloads.flow queuedDownloads = repository.queuedDownloads.flow
activeDownloads = repository.activeDownloads activeDownloads = repository.activeDownloads
processingDownloads = repository.processingDownloads processingDownloads = repository.processingDownloads
savedDownloads = repository.savedDownloads.flow savedDownloads = repository.savedDownloads.flow
scheduledDownloads = repository.scheduledDownloads.flow
cancelledDownloads = repository.cancelledDownloads.flow cancelledDownloads = repository.cancelledDownloads.flow
erroredDownloads = repository.erroredDownloads.flow erroredDownloads = repository.erroredDownloads.flow
viewModelScope.launch(Dispatchers.IO){ viewModelScope.launch(Dispatchers.IO){
@ -652,6 +663,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.deleteCancelled() 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) { fun deleteErrored() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteErrored() repository.deleteErrored()
} }
@ -679,11 +698,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
fun getCancelled() : List<DownloadItem> { fun getCancelled() : List<DownloadItem> {
return repository.getCancelledDownloads() return repository.getCancelledDownloads()
} }
fun getPausedDownloads() : List<DownloadItem> {
return repository.getPausedDownloads()
}
fun getErrored() : List<DownloadItem> { fun getErrored() : List<DownloadItem> {
return repository.getErroredDownloads() return repository.getErroredDownloads()
} }
@ -714,9 +728,65 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.startDownloadWorker(emptyList(), application) repository.startDownloadWorker(emptyList(), application)
} }
suspend fun startDownloadWorker(list: List<DownloadItem>){
repository.startDownloadWorker(list, application)
}
suspend fun putAtTopOfQueue(ids: List<Long>) = CoroutineScope(Dispatchers.IO).launch{ suspend fun putAtTopOfQueue(ids: List<Long>) = CoroutineScope(Dispatchers.IO).launch{
dbManager.downloadDao.putAtTopOfTheQueue(ids) val downloads = dao.getQueuedDownloadsListIDs()
repository.startDownloadWorker(emptyList(), application) 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<Long>()
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<Long>) = 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<Long>()
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<Long>) = CoroutineScope(Dispatchers.IO).launch { suspend fun reQueueDownloadItems(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
@ -734,9 +804,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val existingDownloads = mutableListOf<Long>() val existingDownloads = mutableListOf<Long>()
val existingHistory = mutableListOf<Long>() val existingHistory = mutableListOf<Long>()
var ignoreDuplicate = ign
if (sharedPreferences.getBoolean("download_archive", false)) ignoreDuplicate = true
//if scheduler is on //if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){ if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
@ -747,41 +814,94 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" } 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 { 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 var alreadyExists = false
if(!ignoreDuplicate){ val checkDuplicate = sharedPreferences.getString("prevent_duplicate_downloads", "")!!
val currentCommand = infoUtil.buildYoutubeDLRequest(it) if (checkDuplicate.isNotEmpty() && !ign){
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand) when(checkDuplicate){
val existingDownload = activeAndQueuedDownloads.firstOrNull{d -> "download_archive" -> {
d.id = 0 if (downloadArchive.any { d -> it.url.contains(d) }){
d.logID = null alreadyExists = true
d.customFileNameTemplate = it.customFileNameTemplate if (it.id == 0L) {
d.status = DownloadRepository.Status.Queued.toString() it.status = DownloadRepository.Status.Processing.toString()
d.toString() == it.toString() val id = runBlocking {
} repository.insert(it)
}
if (existingDownload != null){ it.id = id
it.status = DownloadRepository.Status.Saved.toString() }
val id = runBlocking { existingDownloads.add(it.id)
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) } }
} }
"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 { if (existingDownload != null){
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") 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){ val existingHistoryItem = history.firstOrNull {
alreadyExists = true h -> h.type == it.type
existingHistory.add(existingHistoryItem.id) }
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) repository.insert(it)
} }
it.id = id 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){ withContext(Dispatchers.IO){
repository.update(it) repository.update(it)
} }
@ -839,7 +959,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun getQueuedCollectedFileSize() : Long { 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<DownloadRepository.Status>) : LiveData<Int> { fun getTotalSize(status: List<DownloadRepository.Status>) : LiveData<Int> {
@ -862,6 +982,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.getProcessingDownloads().apply { repository.getProcessingDownloads().apply {
if (timeInMillis > 0){ if (timeInMillis > 0){
this.forEach { this.forEach {
it.status = DownloadRepository.Status.Scheduled.toString()
it.downloadStartTime = timeInMillis it.downloadStartTime = timeInMillis
} }
} }
@ -999,8 +1120,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dao.getIDsBetweenTwoItems(item1, item2, statuses) return dao.getIDsBetweenTwoItems(item1, item2, statuses)
} }
fun getQueuedIDsBetweenTwoItems(item1: Long, item2: Long) : List<Long> { fun getScheduledIDsBetweenTwoItems(item1: Long, item2: Long) : List<Long> {
return dao.getQueuedIDsBetweenTwoItems(item1, item2) return dao.getScheduledIDsBetweenTwoItems(item1, item2)
} }

View file

@ -11,6 +11,8 @@ import androidx.work.Constraints
import androidx.work.Data import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem
@ -88,10 +90,6 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
Calendar.getInstance().apply { Calendar.getInstance().apply {
timeInMillis = it.startsTime 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){ if (it.everyCategory != ObserveSourcesRepository.EveryCategory.HOUR){
val hourMin = Calendar.getInstance() val hourMin = Calendar.getInstance()
@ -131,7 +129,7 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
.addTag("observeSources") .addTag("observeSources")
.addTag(it.id.toString()) .addTag(it.id.toString())
.setConstraints(workConstraints.build()) .setConstraints(workConstraints.build())
.setInitialDelay(System.currentTimeMillis() - timeInMillis, TimeUnit.MILLISECONDS) .setInitialDelay(timeInMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setInputData(Data.Builder().putLong("id", it.id).build()) .setInputData(Data.Builder().putLong("id", it.id).build())
workManager.enqueueUniqueWork( workManager.enqueueUniqueWork(

View file

@ -645,7 +645,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
fun scrollToTop() { fun scrollToTop() {
recyclerView!!.scrollToPosition(0) recyclerView!!.scrollToPosition(0)
(searchBar!!.parent as AppBarLayout).setExpanded(true, true) runCatching { (searchBar!!.parent as AppBarLayout).setExpanded(true, true) }
} }
@SuppressLint("ResourceType") @SuppressLint("ResourceType")

View file

@ -116,11 +116,6 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
onItemClickListener.onOutputClick(item) onItemClickListener.onOutputClick(item)
} }
// PAUSE BUTTON ----------------------------------
val pauseButton = card.findViewById<MaterialButton>(R.id.active_download_pause)
if (pauseButton.hasOnClickListeners()) pauseButton.setOnClickListener(null)
// CANCEL BUTTON ---------------------------------- // CANCEL BUTTON ----------------------------------
val cancelButton = card.findViewById<MaterialButton>(R.id.active_download_delete) val cancelButton = card.findViewById<MaterialButton>(R.id.active_download_delete)
if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null) if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null)
@ -130,64 +125,18 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
when(DownloadRepository.Status.valueOf(item.status)){ when(DownloadRepository.Status.valueOf(item.status)){
DownloadRepository.Status.Active -> { DownloadRepository.Status.Active -> {
progressBar.isIndeterminate = true progressBar.isIndeterminate = true
cancelButton.isEnabled = 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
} }
DownloadRepository.Status.ActivePaused -> { DownloadRepository.Status.ActivePaused -> {
progressBar.isIndeterminate = false progressBar.isIndeterminate = false
cancelButton.isEnabled = true
val fromRadius: Int = dp(activity.resources, 15f) output.text = activity.getString(R.string.exo_download_paused)
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
} }
else -> {} 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 { interface OnItemClickListener {
fun onCancelClick(itemID: Long) fun onCancelClick(itemID: Long)
fun onPauseClick(itemID: Long, action: ActiveDownloadAction, position: Int)
fun onOutputClick(item: DownloadItem) fun onOutputClick(item: DownloadItem)
} }

View file

@ -2,14 +2,19 @@ package com.deniscerri.ytdl.ui.adapter
import android.app.Activity import android.app.Activity
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.PopupMenu
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat 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.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@ -18,6 +23,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository 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.loadThumbnail
import com.deniscerri.ytdl.util.Extensions.popup import com.deniscerri.ytdl.util.Extensions.popup
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
@ -42,7 +48,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
val cardView: MaterialCardView val cardView: MaterialCardView
init { 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() card.popup()
val uiHandler = Handler(Looper.getMainLooper()) val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.image_view) val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
// THUMBNAIL ---------------------------------- // THUMBNAIL ----------------------------------
val hideThumb = sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue") val hideThumb = sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue")
@ -78,10 +84,26 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
} }
itemTitle.text = title.ifEmpty { item.url } itemTitle.text = title.ifEmpty { item.url }
//DOWNLOAD TYPE -----------------------------
val type = card.findViewById<TextView>(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<TextView>(R.id.format_note) val formatNote = card.findViewById<TextView>(R.id.format_note)
val formatDetailsText = StringBuilder(item.format.format_note.uppercase()) 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<TextView>(R.id.codec)
val codecText = val codecText =
if (item.format.encoding != "") { if (item.format.encoding != "") {
item.format.encoding.uppercase() item.format.encoding.uppercase()
@ -90,56 +112,61 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
} else { } else {
item.format.acodec.uppercase() item.format.acodec.uppercase()
} }
if (codecText != "" && codecText != "none"){ if (codecText == "" || codecText == "none"){
formatDetailsText.append(" \t\t $codecText") codec.visibility = View.GONE
}
val fileSize = FileUtil.convertFileSize(item.format.filesize)
if (fileSize != "?") formatDetailsText.append(" \t\t $fileSize")
formatDetailsChip.text = formatDetailsText
// PAUSE BUTTON ----------------------------------
val pauseButton = card.findViewById<MaterialButton>(R.id.active_download_pause)
if (pauseButton.hasOnClickListeners()) pauseButton.setOnClickListener(null)
// CANCEL BUTTON ----------------------------------
val cancelButton = card.findViewById<MaterialButton>(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
}else{ }else{
progressBar.isIndeterminate = true codec.visibility = View.VISIBLE
pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_pause_white) codec.text = codecText
pauseButton.contentDescription = activity.getString(R.string.pause)
cancelButton.visibility = View.GONE
pauseButton.tag = ActiveDownloadAdapter.ActiveDownloadAction.Pause
} }
pauseButton.setOnClickListener { val fileSize = card.findViewById<TextView>(R.id.file_size)
if (pauseButton.tag == ActiveDownloadAdapter.ActiveDownloadAction.Pause){ val fileSizeReadable = FileUtil.convertFileSize(item.format.filesize)
onItemClickListener.onPauseClick(item.id, if (fileSizeReadable == "?") fileSize.visibility = View.GONE
ActiveDownloadAdapter.ActiveDownloadAction.Pause, position) else fileSize.text = fileSizeReadable
pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_play_arrow_white)
if (progressBar.progress == 0) progressBar.isIndeterminate = false val menu = card.findViewById<View>(R.id.options)
cancelButton.visibility = View.VISIBLE menu.setOnClickListener {
pauseButton.tag = ActiveDownloadAdapter.ActiveDownloadAction.Resume 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{ }else{
onItemClickListener.onPauseClick(item.id, pause.isVisible = true
ActiveDownloadAdapter.ActiveDownloadAction.Resume, position) resume.isVisible = false
pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_pause_white)
progressBar.isIndeterminate = true
cancelButton.visibility = View.GONE
pauseButton.tag = ActiveDownloadAdapter.ActiveDownloadAction.Pause
} }
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 { card.setOnClickListener {
onItemClickListener.onCardClick() onItemClickListener.onCardClick()
} }

View file

@ -10,6 +10,7 @@ import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@ -76,6 +77,8 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
} }
itemTitle.text = title itemTitle.text = title
card.findViewById<TextView>(R.id.download_type).isVisible = false
// Format Note ---------------------------------- // Format Note ----------------------------------
val formatNote = card.findViewById<TextView>(R.id.format_note) val formatNote = card.findViewById<TextView>(R.id.format_note)
if (item.format.format_note.isNotEmpty()){ if (item.format.format_note.isNotEmpty()){

View file

@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItemSimple import com.deniscerri.ytdl.database.models.DownloadItemSimple
import com.deniscerri.ytdl.database.repository.DownloadRepository 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.loadThumbnail
import com.deniscerri.ytdl.util.Extensions.popup import com.deniscerri.ytdl.util.Extensions.popup
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
@ -79,6 +80,20 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
} }
itemTitle.text = title.ifEmpty { item.url } itemTitle.text = title.ifEmpty { item.url }
//DOWNLOAD TYPE -----------------------------
val type = card.findViewById<TextView>(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<TextView>(R.id.format_note) val formatNote = card.findViewById<TextView>(R.id.format_note)
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility = if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
View.GONE View.GONE

View file

@ -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)!!) btn.backgroundTintList = MaterialColors.getColorStateList(activity, R.attr.colorSurface, ContextCompat.getColorStateList(activity, android.R.color.transparent)!!)
}else{ }else{
thumbnail.alpha = 1f thumbnail.alpha = 1f
thumbnail.colorFilter = null
btn.backgroundTintList = MaterialColors.getColorStateList(activity, R.attr.colorPrimaryContainer, ContextCompat.getColorStateList(activity, android.R.color.transparent)!!) btn.backgroundTintList = MaterialColors.getColorStateList(activity, R.attr.colorPrimaryContainer, ContextCompat.getColorStateList(activity, android.R.color.transparent)!!)
} }

View file

@ -15,7 +15,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository 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.deniscerri.ytdl.util.Extensions.popup
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
@ -76,7 +76,7 @@ class ObserveSourcesAdapter(onItemClickListener: OnItemClickListener, activity:
//INFO //INFO
val info = card.findViewById<Chip>(R.id.info) val info = card.findViewById<Chip>(R.id.info)
val nextTime = item.calculateNextTime() val nextTime = item.calculateNextTimeForObserving()
val c = Calendar.getInstance() val c = Calendar.getInstance()
c.timeInMillis = nextTime c.timeInMillis = nextTime
@ -149,7 +149,7 @@ class ObserveSourcesAdapter(onItemClickListener: OnItemClickListener, activity:
&& oldItem.status == newItem.status && oldItem.status == newItem.status
&& oldItem.retryMissingDownloads == newItem.retryMissingDownloads && oldItem.retryMissingDownloads == newItem.retryMissingDownloads
&& oldItem.runCount == newItem.runCount && oldItem.runCount == newItem.runCount
&& oldItem.everyCategory == newItem.everyCategory && oldItem.calculateNextTimeForObserving() == newItem.calculateNextTimeForObserving()
} }
} }
} }

View file

@ -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<DownloadItemSimple, QueuedDownloadAdapter.ViewHolder>(
DIFF_CALLBACK
) {
private val onItemClickListener: OnItemClickListener
private val activity: Activity
val checkedItems: MutableSet<Long> = 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<ImageView>(R.id.downloads_image_view)
//DRAG HANDLE
val dragView = card.findViewById<View>(R.id.drag_view)
dragView.isVisible = showDragHandle
card.findViewById<View>(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<TextView>(R.id.duration)
duration.text = item.duration
// TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(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<TextView>(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<TextView>(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<TextView>(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<TextView>(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<View>(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<View>(R.id.card_content).setOnClickListener {
if (checkedItems.size > 0 || inverted) {
checkCard(card, item.id, position)
} else {
onItemClickListener.onQueuedCardClick(item.id)
}
}
card.findViewById<View>(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<Long>){
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<DownloadItemSimple> = object : DiffUtil.ItemCallback<DownloadItemSimple>() {
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
}
}
}
}

View file

@ -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<DownloadItemSimple, ScheduledDownloadAdapter.ViewHolder>(
DIFF_CALLBACK
) {
private val onItemClickListener: OnItemClickListener
private val activity: Activity
val checkedItems: MutableSet<Long> = 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<MaterialCardView>(R.id.download_card_view)
card.popup()
if (item == null) return
card.tag = item.id.toString()
//Scheduled Time
val time = mainView.findViewById<TextView>(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<ImageView>(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<TextView>(R.id.duration)
duration.text = item.duration
// TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(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<TextView>(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<TextView>(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<TextView>(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<TextView>(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<MaterialButton>(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<Long>){
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<DownloadItemSimple> = object : DiffUtil.ItemCallback<DownloadItemSimple>() {
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
}
}
}
}

View file

@ -15,6 +15,7 @@ import android.view.inputmethod.InputMethodManager
import android.widget.* import android.widget.*
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem 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.deniscerri.ytdl.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.elevation.SurfaceColors
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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 infoUtil: InfoUtil
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var commandTemplateViewModel: CommandTemplateViewModel private lateinit var commandTemplateViewModel: CommandTemplateViewModel
@ -64,14 +66,17 @@ class AddExtraCommandsDialog(private val item: DownloadItem?, private val callba
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
val view = LayoutInflater.from(context).inflate(R.layout.result_card_details, null) val view = LayoutInflater.from(context).inflate(R.layout.result_card_details, null)
dialog.setContentView(view) dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
if (callback == null){
this.dismiss()
return
}
val behavior = BottomSheetBehavior.from(view.parent as View) val behavior = BottomSheetBehavior.from(view.parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED

View file

@ -29,6 +29,7 @@ import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment 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.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -66,6 +67,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
super.setupDialog(dialog, style) super.setupDialog(dialog, style)
val view = LayoutInflater.from(context).inflate(R.layout.configure_download_bottom_sheet, null) val view = LayoutInflater.from(context).inflate(R.layout.configure_download_bottom_sheet, null)
dialog.setContentView(view) dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
dialog.setOnShowListener { dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()

View file

@ -41,6 +41,7 @@ import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.materialswitch.MaterialSwitch
import com.google.android.material.slider.RangeSlider import com.google.android.material.slider.RangeSlider
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
@ -57,7 +58,7 @@ import java.util.*
import kotlin.properties.Delegates import kotlin.properties.Delegates
class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls : String?, private var chapters: List<ChapterItem>?, private val listener: VideoCutListener) : BottomSheetDialogFragment() { class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private val urls : String? = null, private var chapters: List<ChapterItem>? = null, private val listener: VideoCutListener? = null) : BottomSheetDialogFragment() {
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var infoUtil: InfoUtil private lateinit var infoUtil: InfoUtil
private lateinit var player: ExoPlayer 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 resetBtn : Button
private lateinit var chipGroup : ChipGroup private lateinit var chipGroup : ChipGroup
private lateinit var suggestedLabel : View private lateinit var suggestedLabel : View
private lateinit var item: DownloadItem
private var timeSeconds by Delegates.notNull<Int>() private var timeSeconds by Delegates.notNull<Int>()
private lateinit var selectedCuts: MutableList<String> private lateinit var selectedCuts: MutableList<String>
@ -98,6 +100,14 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
super.setupDialog(dialog, style) super.setupDialog(dialog, style)
val view = LayoutInflater.from(context).inflate(R.layout.cut_video_sheet, null) val view = LayoutInflater.from(context).inflate(R.layout.cut_video_sheet, null)
dialog.setContentView(view) dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
if (_item == null){
this.dismiss()
return
}
item = _item
dialog.setOnShowListener { dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)
@ -251,9 +261,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
rewindBtn.setOnClickListener { rewindBtn.setOnClickListener {
try { try {
val seconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString()) val seconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
val endTimestamp = (rangeSlider.valueTo.toInt() * timeSeconds) / 100 player.seekTo((seconds * 1000).toLong())
val startValue = (seconds.toFloat() / endTimestamp) * 100
player.seekTo((((startValue * timeSeconds) / 100) * 1000).toLong())
player.play() player.play()
}catch (ignored: Exception) {} }catch (ignored: Exception) {}
} }
@ -269,12 +277,15 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n", "ClickableViewAccessibility")
private fun initCutSection(){ private fun initCutSection(){
fromTextInput.editText!!.setTextAndRecalculateWidth("0:00") fromTextInput.editText!!.setTextAndRecalculateWidth("0:00")
toTextInput.editText!!.setTextAndRecalculateWidth(item.duration) 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) { when (event.action) {
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
updateFromSlider() 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 // Return 'false' to allow the event to continue propagating or 'true' to consume it
false false
} }
rangeSlider.performClick() rangeSlider.addOnChangeListener { rangeSlider, fl, b ->
rangeSlider.setOnDragListener { view, dragEvent ->
updateFromSlider()
false
} }
fromTextInput.editText!!.setOnKeyListener(object : View.OnKeyListener { 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) && if ((event!!.action == KeyEvent.ACTION_DOWN) &&
(keyCode == KeyEvent.KEYCODE_ENTER)) { (keyCode == KeyEvent.KEYCODE_ENTER)) {
var startTimestamp = (rangeSlider.valueFrom.toInt() * timeSeconds) / 100 var startTimestamp = rangeSlider.valueFrom.toInt()
val endTimestamp = (rangeSlider.valueTo.toInt() * timeSeconds) / 100 val endTimestamp = rangeSlider.valueTo.toInt()
fromTextInput.editText!!.clearFocus() fromTextInput.editText!!.clearFocus()
var seconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString()) var seconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
val endSeconds = convertStringToTimestamp(toTextInput.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) { if (seconds == 0) {
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US)) fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
}else if (startValue > 100){ }else if (seconds > endSeconds){
startTimestamp = 0 startTimestamp = 0
seconds = 0 seconds = 0
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US)) fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
startValue = 0F
} }
fromTextInput.editText!!.setTextAndRecalculateWidth(fromTextInput.editText!!.text.toString()) fromTextInput.editText!!.setTextAndRecalculateWidth(fromTextInput.editText!!.text.toString())
rangeSlider.setValues(startValue, endValue) rangeSlider.setValues(seconds.toFloat(), endSeconds.toFloat())
okBtn.isEnabled = startValue != 0F || endValue != 100F okBtn.isEnabled = seconds != 0 || endSeconds != timeSeconds
try { try {
player.seekTo(seconds.toLong() * 1000) player.seekTo(seconds.toLong() * 1000)
player.play() player.play()
@ -345,28 +349,26 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
toTextInput.editText!!.clearFocus() toTextInput.editText!!.clearFocus()
val startSeconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString()) 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 if (endSeconds > timeSeconds){
var endValue = (seconds.toFloat() / endTimestamp) * 100
if (endValue > 100F){
endTimestamp = timeSeconds endTimestamp = timeSeconds
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US)) toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
endValue = 100F endSeconds = timeSeconds
} }
if (seconds == 0) { if (endSeconds == 0) {
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US)) 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(endTimestamp.toStringDuration(Locale.US))
} }
toTextInput.editText!!.setTextAndRecalculateWidth(toTextInput.editText!!.text.toString()) toTextInput.editText!!.setTextAndRecalculateWidth(toTextInput.editText!!.text.toString())
rangeSlider.setValues(startValue, endValue) rangeSlider.setValues(startSeconds.toFloat(), endSeconds.toFloat())
okBtn.isEnabled = startValue != 0F || endValue != 100F okBtn.isEnabled = startSeconds != 0 || endSeconds != timeSeconds
try { try {
player.seekTo((seconds.toLong() - 4L) * 1000) player.seekTo((endSeconds.toLong() - 2L) * 1000)
player.play() player.play()
}catch (ignored: Exception) {} }catch (ignored: Exception) {}
@ -400,20 +402,32 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
private fun updateFromSlider(){ private fun updateFromSlider(){
val values = rangeSlider.values val values = rangeSlider.values
val startTimestamp = (values[0].toInt() * timeSeconds) / 100 val startTimestamp = values[0].toInt()
val endTimestamp = (values[1].toInt() * timeSeconds) / 100 val endTimestamp = values[1].toInt()
val startTimestampString = startTimestamp.toStringDuration(Locale.US) val startTimestampString = startTimestamp.toStringDuration(Locale.US)
val endTimestampString = endTimestamp.toStringDuration(Locale.US) val endTimestampString = endTimestamp.toStringDuration(Locale.US)
var draggedFromBeginning = true
if (toTextInput.editText!!.text.toString() != endTimestampString){
draggedFromBeginning = false
}
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestampString) fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestampString)
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestampString) 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 { 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() player.play()
}catch (ignored: Exception) {} }catch (ignored: Exception) {}
} }
@ -452,7 +466,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
newCutBtn.setOnClickListener { newCutBtn.setOnClickListener {
cutSection.visibility = View.VISIBLE cutSection.visibility = View.VISIBLE
cutListSection.visibility = View.GONE cutListSection.visibility = View.GONE
rangeSlider.setValues(0F, 100F) rangeSlider.setValues(0F, timeSeconds.toFloat())
player.seekTo(0) player.seekTo(0)
suggestedChips.children.apply { suggestedChips.children.apply {
this.forEach { this.forEach {
@ -464,7 +478,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
resetBtn.setOnClickListener { resetBtn.setOnClickListener {
chipGroup.removeAllViews() chipGroup.removeAllViews()
listener.onChangeCut(emptyList()) listener?.onChangeCut(emptyList())
player.stop() player.stop()
dismiss() dismiss()
} }
@ -484,20 +498,17 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
val startTimestamp = convertStringToTimestamp(timestamp.split("-")[0].replace(";", "")) val startTimestamp = convertStringToTimestamp(timestamp.split("-")[0].replace(";", ""))
val endTimestamp = convertStringToTimestamp(timestamp.split("-")[1].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 val chip = layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
chip.text = timestamp chip.text = timestamp
chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK)) chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK))
chip.isCheckedIconVisible = false chip.isCheckedIconVisible = false
chipGroup.addView(chip) chipGroup.addView(chip)
selectedCuts.add(chip.text.toString()) selectedCuts.add(chip.text.toString())
listener.onChangeCut(selectedCuts) listener?.onChangeCut(selectedCuts)
chip.setOnClickListener { chip.setOnClickListener {
if (chip.isChecked) { if (chip.isChecked) {
rangeSlider.setValues(startingValue.toFloat(), endingValue.toFloat()) rangeSlider.setValues(startTimestamp.toFloat(), endTimestamp.toFloat())
player.prepare() player.prepare()
player.seekTo((startTimestamp * 1000).toLong()) player.seekTo((startTimestamp * 1000).toLong())
player.play() player.play()
@ -513,7 +524,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
player.pause() player.pause()
chipGroup.removeView(chip) chipGroup.removeView(chip)
selectedCuts.remove(chip.text.toString()) selectedCuts.remove(chip.text.toString())
listener.onChangeCut(selectedCuts) listener?.onChangeCut(selectedCuts)
if (selectedCuts.isEmpty()){ if (selectedCuts.isEmpty()){
player.stop() player.stop()
dismiss() dismiss()
@ -539,21 +550,18 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
if (! selectedCuts.contains(timestamp)) if (! selectedCuts.contains(timestamp))
selectedCuts.add(timestamp) selectedCuts.add(timestamp)
listener.onChangeCut(selectedCuts) listener?.onChangeCut(selectedCuts)
if (chapter.start_time == 0L && chapter.end_time == 0L) { if (chapter.start_time == 0L && chapter.end_time == 0L) {
chip.isEnabled = false chip.isEnabled = false
}else{ }else{
val startTimestamp = chapter.start_time.toInt() val startTimestamp = chapter.start_time.toInt()
val endTimestamp = chapter.end_time.toInt() val endTimestamp = chapter.end_time.toInt()
val startingValue = ((startTimestamp.toFloat() / timeSeconds) * 100).toInt()
val endingValue = ((endTimestamp.toFloat() / timeSeconds) * 100).toInt()
chip.setOnClickListener { chip.setOnClickListener {
if (chip.isChecked) { if (chip.isChecked) {
rangeSlider.setValues(startingValue.toFloat(), endingValue.toFloat()) rangeSlider.setValues(startTimestamp.toFloat(), endTimestamp.toFloat())
player.prepare() player.prepare()
player.seekTo((((startingValue * timeSeconds) / 100) * 1000).toLong()) player.seekTo(((startTimestamp) * 1000).toLong())
player.play() player.play()
}else { }else {
player.seekTo(0) player.seekTo(0)
@ -570,7 +578,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
player.pause() player.pause()
chipGroup.removeView(chip) chipGroup.removeView(chip)
selectedCuts.remove(chip.text.toString()) selectedCuts.remove(chip.text.toString())
listener.onChangeCut(selectedCuts) listener?.onChangeCut(selectedCuts)
if (selectedCuts.isEmpty()){ if (selectedCuts.isEmpty()){
player.stop() player.stop()
dismiss() dismiss()

View file

@ -46,6 +46,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
@ -127,6 +128,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
super.setupDialog(dialog, style) super.setupDialog(dialog, style)
view = LayoutInflater.from(context).inflate(R.layout.download_bottom_sheet, null) view = LayoutInflater.from(context).inflate(R.layout.download_bottom_sheet, null)
dialog.setContentView(view) dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
dialog.setOnShowListener { dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)
@ -313,6 +315,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
scheduleBtn.isEnabled = false scheduleBtn.isEnabled = false
download.isEnabled = false download.isEnabled = false
val item: DownloadItem = getDownloadItem() val item: DownloadItem = getDownloadItem()
item.status = DownloadRepository.Status.Scheduled.toString()
item.downloadStartTime = it.timeInMillis item.downloadStartTime = it.timeInMillis
if (item.videoPreferences.alsoDownloadAsAudio){ if (item.videoPreferences.alsoDownloadAsAudio){
val itemsToQueue = mutableListOf<DownloadItem>() val itemsToQueue = mutableListOf<DownloadItem>()

View file

@ -80,8 +80,6 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
if (type != DownloadViewModel.Type.command){ if (type != DownloadViewModel.Type.command){
type = DownloadViewModel.Type.command type = DownloadViewModel.Type.command
} }
format = downloadViewModel.getFormat(allFormats, DownloadViewModel.Type.command)
} }
val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java) val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)

View file

@ -48,6 +48,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -92,6 +93,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
super.setupDialog(dialog, style) super.setupDialog(dialog, style)
val view = LayoutInflater.from(context).inflate(R.layout.download_multiple_bottom_sheet, null) val view = LayoutInflater.from(context).inflate(R.layout.download_multiple_bottom_sheet, null)
dialog.setContentView(view) dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
dialog.setOnShowListener { dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)

View file

@ -36,7 +36,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, private var formats: List<List<Format>>, private val listener: OnFormatClickListener) : BottomSheetDialogFragment() { class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>? = null, private var _formats: List<List<Format>>? = null, private val _listener: OnFormatClickListener? = null) : BottomSheetDialogFragment() {
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var infoUtil: InfoUtil private lateinit var infoUtil: InfoUtil
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
@ -62,6 +62,10 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private var hasGenericFormats: Boolean = false private var hasGenericFormats: Boolean = false
private lateinit var items: List<DownloadItem?>
private lateinit var formats: List<List<Format>>
private lateinit var listener: OnFormatClickListener
enum class FormatSorting { enum class FormatSorting {
filesize, container, codec, id filesize, container, codec, id
} }
@ -87,6 +91,15 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
view = LayoutInflater.from(context).inflate(R.layout.format_select_bottom_sheet, null) view = LayoutInflater.from(context).inflate(R.layout.format_select_bottom_sheet, null)
dialog.setContentView(view) dialog.setContentView(view)
if (_items == null){
this.dismiss()
return
}
items = _items
formats = _formats!!
listener = _listener!!
sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!) sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!)
filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!) filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!)
filterBtn = view.findViewById(R.id.format_filter) filterBtn = view.findViewById(R.id.format_filter)
@ -368,9 +381,10 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
FormatSorting.codec -> { FormatSorting.codec -> {
val codecOrder = resources.getStringArray(R.array.video_codec_values).toMutableList() val codecOrder = resources.getStringArray(R.array.video_codec_values).toMutableList()
codecOrder.removeFirst() codecOrder.removeFirst()
chosenFormats.groupBy { format -> codecOrder.indexOfFirst { format.vcodec.startsWith(it) } } chosenFormats.groupBy { format -> codecOrder.indexOfFirst { format.vcodec.matches("^(${it})(.+)?$".toRegex()) } }
.flatMap { .flatMap {
it.value.sortedBy { l -> l.filesize } it.value.sortedByDescending { l -> l.filesize }
} }
} }
FormatSorting.filesize -> chosenFormats FormatSorting.filesize -> chosenFormats

View file

@ -47,6 +47,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup 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.materialswitch.MaterialSwitch
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
@ -128,6 +129,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
super.setupDialog(dialog, style) super.setupDialog(dialog, style)
view = LayoutInflater.from(context).inflate(R.layout.observe_sources_bottom_sheet, null) view = LayoutInflater.from(context).inflate(R.layout.observe_sources_bottom_sheet, null)
dialog.setContentView(view) dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
dialog.setOnShowListener { dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)

View file

@ -59,6 +59,7 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
@ -123,6 +124,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
super.setupDialog(dialog, style) super.setupDialog(dialog, style)
val view = LayoutInflater.from(context).inflate(R.layout.result_card_details, null) val view = LayoutInflater.from(context).inflate(R.layout.result_card_details, null)
dialog.setContentView(view) dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
dialog.setOnShowListener { dialog.setOnShowListener {
val behavior = BottomSheetBehavior.from(dialogView.parent as View) val behavior = BottomSheetBehavior.from(dialogView.parent as View)
@ -613,7 +615,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
} }
ActiveDownloadAdapter.ActiveDownloadAction.Resume -> { ActiveDownloadAdapter.ActiveDownloadAction.Resume -> {
lifecycleScope.launch { lifecycleScope.launch {
item.status = DownloadRepository.Status.PausedReQueued.toString() item.status = DownloadRepository.Status.Queued.toString()
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item) downloadViewModel.updateDownload(item)
} }

View file

@ -29,6 +29,7 @@ import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton 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.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -77,6 +78,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
super.setupDialog(dialog, style) super.setupDialog(dialog, style)
val view = LayoutInflater.from(context).inflate(R.layout.select_playlist_items, null) val view = LayoutInflater.from(context).inflate(R.layout.select_playlist_items, null)
dialog.setContentView(view) dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
dialog.setOnShowListener { dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)

View file

@ -1,32 +1,55 @@
package com.deniscerri.ytdl.ui.downloads package com.deniscerri.ytdl.ui.downloads
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.DialogInterface
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.View.OnClickListener
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView 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.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkQuery import androidx.work.WorkQuery
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter 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.forceFastScrollMode
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.NotificationUtil 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.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -35,15 +58,17 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener, OnClickListener { class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener {
private var fragmentView: View? = null private var fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var activeRecyclerView : RecyclerView private lateinit var activeRecyclerView : RecyclerView
private lateinit var activeDownloads : ActiveDownloadAdapter private lateinit var activeDownloads : ActiveDownloadAdapter
lateinit var downloadItem: DownloadItem
private lateinit var notificationUtil: NotificationUtil 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 noResults: RelativeLayout
private lateinit var workManager: WorkManager private lateinit var workManager: WorkManager
@ -57,6 +82,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
return fragmentView return fragmentView
} }
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -65,75 +91,73 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
workManager = WorkManager.getInstance(requireContext()) workManager = WorkManager.getInstance(requireContext())
activeDownloads = activeDownloads = ActiveDownloadAdapter(this,requireActivity())
ActiveDownloadAdapter(
this,
requireActivity()
)
activeRecyclerView = view.findViewById(R.id.download_recyclerview) activeRecyclerView = view.findViewById(R.id.download_recyclerview)
activeRecyclerView.forceFastScrollMode() activeRecyclerView.forceFastScrollMode()
activeRecyclerView.adapter = activeDownloads activeRecyclerView.adapter = activeDownloads
activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) 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) noResults = view.findViewById(R.id.no_results)
pauseResume.setOnClickListener { pause.setOnClickListener {
if (pauseResume.text == requireContext().getString(R.string.pause)){ lifecycleScope.launch {
lifecycleScope.launch { workManager.cancelAllWorkByTag("download")
workManager.cancelAllWorkByTag("download") pause.isEnabled = false
pauseResume.isEnabled = false
// pause queued // pause queued
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.getQueued() downloadViewModel.getQueued()
}.forEach { }.forEach {
it.status = DownloadRepository.Status.QueuedPaused.toString() it.status = DownloadRepository.Status.QueuedPaused.toString()
downloadViewModel.updateDownload(it) 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
} }
}else{
lifecycleScope.launch {
pauseResume.isEnabled = false
val active = withContext(Dispatchers.IO){ // pause active
downloadViewModel.getActiveDownloads() 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 { toQueue.forEach {
it.status = DownloadRepository.Status.Queued.toString() it.status = DownloadRepository.Status.Queued.toString()
downloadViewModel.updateDownload(it) 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 { lifecycleScope.launch {
downloadViewModel.activeDownloadsCount.collectLatest { downloadViewModel.activeDownloadsCount.collectLatest {
delay(200) pause.isVisible = it > 0
noResults.visibility = if (it == 0) View.VISIBLE else View.GONE }
}
lifecycleScope.launch {
downloadViewModel.pausedDownloadsCount.collectLatest {
resume.isVisible = it > 0
} }
} }
@ -171,19 +208,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
downloadViewModel.activeDownloads.collectLatest { downloadViewModel.activeDownloads.collectLatest {
delay(100) delay(100)
activeDownloads.submitList(it) 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) 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) { override fun onOutputClick(item: DownloadItem) {
if (item.logID != null && item.logID != 0L) { if (item.logID != null && item.logID != 0L) {
val bundle = Bundle() val bundle = Bundle()
@ -279,7 +264,4 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
} }
} }
} }
override fun onClick(p0: View?) {
}
} }

View file

@ -4,6 +4,8 @@ import android.content.DialogInterface
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -15,6 +17,7 @@ import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import androidx.work.WorkManager import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.inflate
import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
@ -69,7 +72,7 @@ class DownloadQueueMainFragment : Fragment(){
overScrollMode = View.OVER_SCROLL_NEVER overScrollMode = View.OVER_SCROLL_NEVER
} }
val fragments = mutableListOf(ActiveDownloadsFragment(), QueuedDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment(), SavedDownloadsFragment()) val fragments = mutableListOf(ActiveDownloadsFragment(), QueuedDownloadsFragment(), ScheduledDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment(), SavedDownloadsFragment())
fragmentAdapter = DownloadListFragmentAdapter( fragmentAdapter = DownloadListFragmentAdapter(
childFragmentManager, childFragmentManager,
@ -84,9 +87,10 @@ class DownloadQueueMainFragment : Fragment(){
when (position) { when (position) {
0 -> tab.text = getString(R.string.running) 0 -> tab.text = getString(R.string.running)
1 -> tab.text = getString(R.string.in_queue) 1 -> tab.text = getString(R.string.in_queue)
2 -> tab.text = getString(R.string.cancelled) 2 -> tab.text = getString(R.string.scheduled)
3 -> tab.text = getString(R.string.errored) 3 -> tab.text = getString(R.string.cancelled)
4 -> tab.text = getString(R.string.saved) 4 -> tab.text = getString(R.string.errored)
5 -> tab.text = getString(R.string.saved)
} }
}.attach() }.attach()
@ -105,6 +109,7 @@ class DownloadQueueMainFragment : Fragment(){
viewPager2.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() { viewPager2.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
tabLayout.selectTab(tabLayout.getTabAt(position)) tabLayout.selectTab(tabLayout.getTabAt(position))
initMenu()
} }
}) })
mainActivity.hideBottomNavigation() mainActivity.hideBottomNavigation()
@ -133,32 +138,41 @@ class DownloadQueueMainFragment : Fragment(){
} }
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.cancelledDownloadsCount.collectLatest { downloadViewModel.scheduledDownloadsCount.collectLatest {
tabLayout.getTabAt(2)?.apply { tabLayout.getTabAt(2)?.apply {
createBadge(it) createBadge(it)
} }
} }
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.erroredDownloadsCount.collectLatest { downloadViewModel.cancelledDownloadsCount.collectLatest {
tabLayout.getTabAt(3)?.apply { tabLayout.getTabAt(3)?.apply {
createBadge(it) createBadge(it)
} }
} }
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.savedDownloadsCount.collectLatest { downloadViewModel.erroredDownloadsCount.collectLatest {
tabLayout.getTabAt(4)?.apply { tabLayout.getTabAt(4)?.apply {
removeBadge() removeBadge()
if (it > 0) createBadge(it) if (it > 0) createBadge(it)
} }
} }
} }
lifecycleScope.launch {
downloadViewModel.savedDownloadsCount.collectLatest {
tabLayout.getTabAt(5)?.apply {
removeBadge()
if (it > 0) createBadge(it)
}
}
}
} }
} }
private fun initMenu() { private fun initMenu() {
topAppBar.setOnMenuItemClickListener { m: MenuItem -> topAppBar.setOnMenuItemClickListener { m: MenuItem ->
try{ try{
when(m.itemId){ when(m.itemId){
@ -172,6 +186,11 @@ class DownloadQueueMainFragment : Fragment(){
downloadViewModel.deleteCancelled() downloadViewModel.deleteCancelled()
} }
} }
R.id.clear_scheduled -> {
showDeleteDialog {
downloadViewModel.deleteScheduled()
}
}
R.id.clear_errored -> { R.id.clear_errored -> {
showDeleteDialog { showDeleteDialog {
downloadViewModel.deleteErrored() downloadViewModel.deleteErrored()
@ -185,11 +204,12 @@ class DownloadQueueMainFragment : Fragment(){
R.id.copy_urls -> { R.id.copy_urls -> {
lifecycleScope.launch { lifecycleScope.launch {
val tabStatus = mapOf( val tabStatus = mapOf(
0 to listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused, DownloadRepository.Status.PausedReQueued), 0 to listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused),
1 to listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.Queued), 1 to listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused),
2 to listOf(DownloadRepository.Status.Cancelled), 2 to listOf(DownloadRepository.Status.Scheduled),
3 to listOf(DownloadRepository.Status.Error), 3 to listOf(DownloadRepository.Status.Cancelled),
4 to listOf(DownloadRepository.Status.Saved), 4 to listOf(DownloadRepository.Status.Error),
5 to listOf(DownloadRepository.Status.Saved),
) )
tabStatus[tabLayout.selectedTabPosition]?.apply { tabStatus[tabLayout.selectedTabPosition]?.apply {
val urls = withContext(Dispatchers.IO){ val urls = withContext(Dispatchers.IO){
@ -233,9 +253,4 @@ class DownloadQueueMainFragment : Fragment(){
} }
} }
} }
companion object {
private const val TAG = "DownloadQueueActivity"
}
} }

View file

@ -1,22 +1,26 @@
package com.deniscerri.ytdl.ui.downloads package com.deniscerri.ytdl.ui.downloads
import android.animation.AnimatorSet
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.DialogInterface import android.content.DialogInterface
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -31,7 +35,7 @@ import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel 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.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdl.util.Extensions.toListString 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.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetDialog 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.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -51,15 +56,16 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener { class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickListener {
private var fragmentView: View? = null private var fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var queuedRecyclerView : RecyclerView private lateinit var queuedRecyclerView : RecyclerView
private lateinit var adapter : GenericDownloadAdapter private lateinit var adapter : QueuedDownloadAdapter
private lateinit var noResults : RelativeLayout private lateinit var noResults : RelativeLayout
private lateinit var notificationUtil: NotificationUtil private lateinit var notificationUtil: NotificationUtil
private lateinit var fileSize: TextView private lateinit var fileSize: TextView
private lateinit var dragHandleToggle: TextView
private var totalSize: Int = 0 private var totalSize: Int = 0
private var actionMode : ActionMode? = null private var actionMode : ActionMode? = null
@ -75,21 +81,20 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
return fragmentView return fragmentView
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n", "RestrictedApi")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
fileSize = view.findViewById(R.id.filesize) fileSize = view.findViewById(R.id.filesize)
adapter = dragHandleToggle = view.findViewById(R.id.drag)
GenericDownloadAdapter( val itemTouchHelper = ItemTouchHelper(queuedDragDropHelper)
this, adapter = QueuedDownloadAdapter(this, requireActivity(), itemTouchHelper)
requireActivity()
)
noResults = view.findViewById(R.id.no_results) noResults = view.findViewById(R.id.no_results)
queuedRecyclerView = view.findViewById(R.id.download_recyclerview) queuedRecyclerView = view.findViewById(R.id.download_recyclerview)
queuedRecyclerView.forceFastScrollMode() queuedRecyclerView.forceFastScrollMode()
queuedRecyclerView.adapter = adapter queuedRecyclerView.adapter = adapter
queuedRecyclerView.enableFastScroll() queuedRecyclerView.enableFastScroll()
itemTouchHelper.attachToRecyclerView(queuedRecyclerView)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("queued")){ 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){ downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused)).observe(viewLifecycleOwner){
totalSize = it 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) { dragHandleToggle.setOnClickListener {
removeItem(itemID) adapter.toggleShowDragHandle()
}
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()
}
}
}
} }
} }
@ -255,7 +189,8 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
lifecycleScope.launch { lifecycleScope.launch {
val selectedIDs = getSelectedIDs().sortedBy { it } val selectedIDs = getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){ 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() }.toMutableList()
idsInMiddle.addAll(selectedIDs) idsInMiddle.addAll(selectedIDs)
if (idsInMiddle.isNotEmpty()){ if (idsInMiddle.isNotEmpty()){
@ -324,6 +259,17 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
} }
true true
} }
R.id.down -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
adapter.clearCheckedItems()
withContext(Dispatchers.IO){
downloadViewModel.putAtBottomOfQueue(selectedObjects)
}
actionMode?.finish()
}
true
}
R.id.copy_urls -> { R.id.copy_urls -> {
lifecycleScope.launch { lifecycleScope.launch {
val selectedObjects = getSelectedIDs() 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)
}
} }

View file

@ -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<Long>{
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
)
}
}
}

View file

@ -107,6 +107,7 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
val useCookiesPref = preferences.getBoolean("use_cookies", false) val useCookiesPref = preferences.getBoolean("use_cookies", false)
useCookies.isChecked = useCookiesPref useCookies.isChecked = useCookiesPref
useCookies.jumpDrawablesToCurrentState()
newCookie.isEnabled = useCookiesPref newCookie.isEnabled = useCookiesPref
} }

View file

@ -170,6 +170,7 @@ class DownloadLogFragment : Fragment() {
if (logItem != null){ if (logItem != null){
if (logItem.content.isNotBlank()) { if (logItem.content.isNotBlank()) {
content.setText(logItem.content, TextView.BufferType.SPANNABLE) 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){ if (!bottomAppBar.menu.children.first { it.itemId == R.id.scroll_down }.isVisible){
content.scrollTo(0, content.height) content.scrollTo(0, content.height)

View file

@ -1,7 +1,9 @@
package com.deniscerri.ytdl.ui.more.settings package com.deniscerri.ytdl.ui.more.settings
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.core.content.edit
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -14,6 +16,8 @@ import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.DownloadWorker 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.Calendar
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -25,6 +29,16 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
setPreferencesFromResource(R.xml.downloading_preferences, rootKey) setPreferencesFromResource(R.xml.downloading_preferences, rootKey)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
findPreference<Preference>("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<SwitchPreferenceCompat>("remember_download_type") val rememberDownloadType = findPreference<SwitchPreferenceCompat>("remember_download_type")
val downloadType = findPreference<ListPreference>("preferred_download_type") val downloadType = findPreference<ListPreference>("preferred_download_type")
downloadType?.isEnabled = rememberDownloadType?.isChecked == false downloadType?.isEnabled = rememberDownloadType?.isChecked == false

View file

@ -4,11 +4,13 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.provider.Settings import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
@ -112,15 +114,22 @@ class FolderSettingsFragment : BaseSettingsFragment() {
commandPathResultLauncher.launch(intent) commandPathResultLauncher.launch(intent)
true true
} }
accessAllFiles!!.onPreferenceClickListener = if(VERSION.SDK_INT >= 30){
Preference.OnPreferenceClickListener { accessAllFiles!!.onPreferenceClickListener =
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) Preference.OnPreferenceClickListener {
val uri = Uri.parse("package:" + requireContext().packageName) val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.data = uri val uri = Uri.parse("package:" + requireContext().packageName)
startActivity(intent) intent.data = uri
true startActivity(intent)
} true
}
}
if (noFragments!!.isChecked) {
editor.putBoolean("keep_cache", false).apply()
keepFragments!!.isChecked = false
keepFragments!!.isEnabled = false
}
noFragments!!.setOnPreferenceChangeListener { _, newValue -> noFragments!!.setOnPreferenceChangeListener { _, newValue ->
if(newValue as Boolean){ if(newValue as Boolean){
editor.putBoolean("keep_cache", false).apply() editor.putBoolean("keep_cache", false).apply()

View file

@ -115,7 +115,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
showTerminalShareIcon!!.onPreferenceChangeListener = showTerminalShareIcon!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { pref: Preference?, _: Any -> Preference.OnPreferenceChangeListener { pref: Preference?, _: Any ->
val packageManager = requireContext().packageManager 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){ if ((pref as SwitchPreferenceCompat).isChecked){
packageManager.setComponentEnabledSetting(aliasComponentName, packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,

View file

@ -1,6 +1,7 @@
package com.deniscerri.ytdl.util package com.deniscerri.ytdl.util
import android.content.Context import android.content.Context
import android.util.Log
import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.LogItem 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) { override fun uncaughtException(p0: Thread, p1: Throwable) {
p1.message?.apply { p1.message?.apply {
Log.e("ERROR", this)
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
createLog(this@apply) createLog(this@apply)
} }

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdl.util package com.deniscerri.ytdl.util
import android.R.color
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
@ -9,7 +10,9 @@ import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Outline 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.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape import android.graphics.drawable.shapes.OvalShape
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
@ -27,6 +30,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.Px import androidx.annotation.Px
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStarted import androidx.lifecycle.withStarted
@ -335,10 +339,13 @@ object Extensions {
return cookie.toString() return cookie.toString()
} }
fun ObserveSourcesItem.calculateNextTime() : Long { fun ObserveSourcesItem.calculateNextTimeForObserving() : Long {
val item = this val item = this
val now = System.currentTimeMillis()
Calendar.getInstance().apply { 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() val hourMin = Calendar.getInstance()
hourMin.timeInMillis = item.everyTime hourMin.timeInMillis = item.everyTime
@ -346,38 +353,40 @@ object Extensions {
set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE)) set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE))
} }
when(item.everyCategory){ while (timeInMillis < now){
EveryCategory.HOUR -> { when(item.everyCategory){
add(Calendar.HOUR, item.everyNr) EveryCategory.HOUR -> {
} add(Calendar.HOUR, item.everyNr)
EveryCategory.DAY -> { }
add(Calendar.DAY_OF_MONTH, item.everyNr) EveryCategory.DAY -> {
} add(Calendar.DAY_OF_MONTH, item.everyNr)
EveryCategory.WEEK -> { }
item.weeklyConfig?.apply { EveryCategory.WEEK -> {
if (this.weekDays.isEmpty()){ item.weeklyConfig?.apply {
add(Calendar.DAY_OF_MONTH, 7 * item.everyNr) if (this.weekDays.isEmpty()){
}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) 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 -> {
EveryCategory.MONTH -> { add(Calendar.MONTH, item.everyNr)
add(Calendar.MONTH, item.everyNr) item.monthlyConfig?.apply {
item.monthlyConfig?.apply { set(Calendar.DAY_OF_MONTH, this.everyMonthDay)
set(Calendar.DAY_OF_MONTH, this.everyMonthDay) }
} }
} }
} }
@ -392,7 +401,14 @@ object Extensions {
drawable!!.intrinsicWidth, drawable!!.intrinsicWidth,
drawable.intrinsicHeight, Bitmap.Config.ARGB_8888 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) val canvas = Canvas(bitmap)
DrawableCompat.setTint(drawable, colorValue.data)
drawable.setBounds(0, 0, canvas.width, canvas.height) drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas) drawable.draw(canvas)
return bitmap return bitmap

View file

@ -763,6 +763,14 @@ class InfoUtil(private val context: Context) {
format.put("filesize", size) format.put("filesize", size)
}catch (ignored: Exception){} }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) val formatProper = Gson().fromJson(format.toString(), Format::class.java)
if (formatProper.format_note == null) continue 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)) request.addOption("--download-archive", FileUtil.getDownloadArchivePath(context))
} }
@ -1152,7 +1160,7 @@ class InfoUtil(private val context: Context) {
if (embedMetadata){ if (embedMetadata){
request.addOption("--embed-metadata") request.addOption("--embed-metadata")
request.addOption("--parse-metadata", "artist:(?P<meta_album_artist>[^,]+)") request.addOption("--parse-metadata", "artist:(?P<meta_album_artist>[^,]+)")
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<dscrptn_year>\\d{4})") request.addOption("--parse-metadata", "description:(?:Released on: )(?P<dscrptn_year>\\d{4})")
request.addOption("--parse-metadata", "%(dscrptn_year,release_year,release_date>%Y,upload_date>%Y)s:%(meta_date)s") 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) 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){ if (downloadItem.audioPreferences.embedThumb){
request.addOption("--embed-thumbnail") request.addOption("--embed-thumbnail")
if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!) 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 (cropThumb){
if (thumbnailFormat == "jpg") {
thumbnailConfig.deleteCharAt(thumbnailConfig.length - 1)
thumbnailConfig.append(""" $cropConfig""")
}
else thumbnailConfig.append("""--ppa "ThumbnailsConvertor:$cropConfig""")
}
if (thumbnailConfig.isNotBlank()){
runCatching { runCatching {
val config = File(context.cacheDir.absolutePath + "/config" + downloadItem.id + "##ffmpegCrop.txt") 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(thumbnailConfig.toString())
config.writeText(configData)
request.addOption("--config", config.absolutePath) request.addOption("--config", config.absolutePath)
} }
} }
} }
if (filenameTemplate.isNotBlank()){ if (filenameTemplate.isNotBlank()){
@ -1220,19 +1236,20 @@ class InfoUtil(private val context: Context) {
//format logic //format logic
var videoF = downloadItem.format.format_id 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 } val format = downloadItem.allFormats.find { it.format_id == f }
format?.run { format?.run {
if (this.format_id.contains("-")) { if (this.format_id.contains("-")) {
if (!this.lang.isNullOrBlank() && this.lang != "None") { if (!this.lang.isNullOrBlank() && this.lang != "None") {
"ba[format_id~='^(${this.format_id.split("-")[0]})'][language^=${this.lang}]" "ba[format_id~='^(${this.format_id.split("-")[0]})'][language^=${this.lang}]"
} else { } else {
"${this.format_id}/${this.format_id.split("-")[0]}" altAudioF = this.format_id.split("-")[0]
this.format_id
} }
} else this.format_id } else this.format_id
} ?: f } ?: f
}.joinToString("+").ifBlank { "ba" }
}.ifBlank { "ba" }
val preferredAudioLanguage = sharedPreferences.getString("audio_language", "")!! val preferredAudioLanguage = sharedPreferences.getString("audio_language", "")!!
if (downloadItem.videoPreferences.removeAudio) audioF = "" 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 defaultFormats = context.resources.getStringArray(R.array.video_formats_values)
val usingGenericFormat = defaultFormats.contains(videoF) || downloadItem.allFormats.isEmpty() || downloadItem.allFormats == getGenericVideoFormats(context.resources) val usingGenericFormat = defaultFormats.contains(videoF) || downloadItem.allFormats.isEmpty() || downloadItem.allFormats == getGenericVideoFormats(context.resources)
var hasGenericResulutionFormat = ""
if (!usingGenericFormat){ if (!usingGenericFormat){
// with audio removed // with audio removed
if (audioF.isBlank()){ if (audioF.isBlank()){
f.append("$videoF/bv/b") f.append("$videoF/bv/b")
}else{ }else{
//with audio //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){ if (audioF.count("+") > 0){
request.addOption("--audio-multistreams") request.addOption("--audio-multistreams")
@ -1263,7 +1285,8 @@ class InfoUtil(private val context: Context) {
videoF = "wv" videoF = "wv"
if (audioF == "ba") audioF = "wa" if (audioF == "ba") audioF = "wa"
}else if (defaultFormats.contains(videoF)) { }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() val preferredFormatIDs = sharedPreferences.getString("format_id", "").toString()
@ -1331,6 +1354,7 @@ class InfoUtil(private val context: Context) {
} }
StringBuilder().apply { StringBuilder().apply {
if (hasGenericResulutionFormat.isNotBlank()) append(",res:${hasGenericResulutionFormat}")
if (sharedPreferences.getBoolean("prefer_smaller_formats", false)) append(",+size") if (sharedPreferences.getBoolean("prefer_smaller_formats", false)) append(",+size")
if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref") if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref")
if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref") if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref")
@ -1369,7 +1393,9 @@ class InfoUtil(private val context: Context) {
if (downloadItem.videoPreferences.removeAudio){ 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) 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 audioFormatsValues = resources.getStringArray(R.array.audio_formats_values)
val formats = mutableListOf<Format>() val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("audio_format", "") 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.forEachIndexed { idx, it -> formats.add(Format(audioFormatsValues[idx], containerPreference!!,"",acodecPreference!!, "",0, it)) }
audioFormats.reverse() audioFormats.reverse()
audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",resources.getString(R.string.preferred_format_id), "",1, it)) } audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",resources.getString(R.string.preferred_format_id), "",1, it)) }

View file

@ -8,6 +8,7 @@ import android.app.TaskStackBuilder
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -243,19 +244,18 @@ class NotificationUtil(var context: Context) {
} }
val bitmap = iconType.toBitmap(context) val bitmap = iconType.toBitmap(context)
val bigPictureStyle = NotificationCompat.BigPictureStyle()
bigPictureStyle.bigLargeIcon(bitmap)
if (Build.VERSION.SDK_INT >= 31){
bigPictureStyle.showBigPictureWhenCollapsed(true)
}
notificationBuilder notificationBuilder
.setContentTitle("${res.getString(R.string.downloaded)} $title") .setContentTitle("${res.getString(R.string.downloaded)} $title")
.setSmallIcon(R.drawable.ic_launcher_foreground_large) .setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon(bitmap)
.setGroup(DOWNLOAD_FINISHED_NOTIFICATION_ID.toString()) .setGroup(DOWNLOAD_FINISHED_NOTIFICATION_ID.toString())
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.setContentText(title) .setContentText(title)
.setStyle(bigPictureStyle) .setStyle(NotificationCompat.BigTextStyle()
.bigText("""
$title
${filepath?.joinToString("\n")}
""".trimIndent()))
.setPriority(NotificationCompat.PRIORITY_MAX) .setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions() .clearActions()
@ -404,7 +404,7 @@ class NotificationUtil(var context: Context) {
) )
try { try {
notificationBuilder.setProgress(100, progress, progress == 0) notificationBuilder.setProgress(100, progress, (progress == 0 || progress == 100))
.setContentTitle(title) .setContentTitle(title)
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) .setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
.clearActions() .clearActions()

View file

@ -1,12 +1,18 @@
package com.deniscerri.ytdl.util package com.deniscerri.ytdl.util
import android.animation.Animator
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.ClipData import android.content.ClipData
import android.content.ClipData.Item
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.text.Editable import android.text.Editable
@ -26,10 +32,13 @@ import android.widget.EditText
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.DimenRes
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.cardview.widget.CardView
import androidx.compose.ui.graphics.Color
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -39,6 +48,7 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R 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.materialswitch.MaterialSwitch
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout 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.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat import com.google.android.material.timepicker.TimeFormat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -696,11 +708,11 @@ object UiUtil {
true true
} }
} }
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused -> { DownloadRepository.Status.Queued -> {
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) download!!.visibility = View.GONE download!!.visibility = View.GONE
else{ }
download!!.text = context.getString(R.string.download_now) DownloadRepository.Status.Scheduled -> {
} download!!.text = context.getString(R.string.download_now)
} }
else -> { else -> {
download?.setOnLongClickListener { download?.setOnLongClickListener {
@ -1803,9 +1815,10 @@ object UiUtil {
} }
} }
sheet.setOnDismissListener { sheet.setOnDismissListener { _ ->
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteProcessing() downloadViewModel.deleteProcessing()
downloadViewModel.deleteAllWithID(it.downloadItems)
} }
} }
@ -2023,7 +2036,10 @@ object UiUtil {
builder.setTitle(context.getString(R.string.piped_instance)) builder.setTitle(context.getString(R.string.piped_instance))
val view = context.layoutInflater.inflate(R.layout.filename_template_dialog, null) val view = context.layoutInflater.inflate(R.layout.filename_template_dialog, null)
val editText = view.findViewById<EditText>(R.id.filename_edittext) val editText = view.findViewById<EditText>(R.id.filename_edittext)
view.findViewById<TextInputLayout>(R.id.filename).hint = context.getString(R.string.piped_instance) view.findViewById<TextInputLayout>(R.id.filename).apply {
hint = context.getString(R.string.piped_instance)
endIconMode = END_ICON_NONE
}
editText.setText(currentInstance) editText.setText(currentInstance)
editText.setSelection(editText.text.length) editText.setSelection(editText.text.length)
builder.setView(view) builder.setView(view)
@ -2142,4 +2158,28 @@ object UiUtil {
val dialog = builder.create() val dialog = builder.create()
dialog.show() 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
}
}
}
} }

View file

@ -64,10 +64,11 @@ class DownloadWorker(
val alarmScheduler = AlarmScheduler(context) val alarmScheduler = AlarmScheduler(context)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val time = System.currentTimeMillis() + 6000 val time = System.currentTimeMillis() + 6000
val queuedItems = dao.getQueuedDownloadsThatAreNotScheduledChunked(time) val queuedItems = dao.getQueuedScheduledDownloadsUntil(time)
val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await() val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await()
if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success() 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 createResultItem = inputData.getBoolean("createResultItem", true)
val confTmp = Configuration(context.resources.configuration) val confTmp = Configuration(context.resources.configuration)
@ -176,21 +177,31 @@ class DownloadWorker(
} }
val wasQuickDownloaded = resultDao.getCountInt() == 0 val wasQuickDownloaded = resultDao.getCountInt() == 0
runBlocking { runBlocking {
var finalPaths : List<String>? var finalPaths : MutableList<String>?
if (noCache){ if (noCache){
setProgressAsync(workDataOf("progress" to 100, "output" to "Scanning Files", "id" to downloadItem.id)) setProgressAsync(workDataOf("progress" to 100, "output" to "Scanning Files", "id" to downloadItem.id))
finalPaths = it.out.split("\n") val outputSequence = it.out.split("\n")
.asSequence() finalPaths =
outputSequence.asSequence()
.filter { it.startsWith("'/storage") } .filter { it.startsWith("'/storage") }
.map { it.removePrefix("'") } .map { it.removeSuffix("\n") }
.map { it.removeSuffix("\n") } .map { it.removeSurrounding("'", "'") }
.map { it.removeSuffix("'") } .toMutableList()
.sortedBy { File(it).lastModified() }
.toList() 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) FileUtil.scanMedia(finalPaths, context)
if (finalPaths.isEmpty()){ if (finalPaths.isEmpty()){
finalPaths = listOf(context.getString(R.string.unfound_file)) finalPaths = mutableListOf(context.getString(R.string.unfound_file))
} }
}else{ }else{
//move file from internal to set download directory //move file from internal to set download directory
@ -200,15 +211,15 @@ class DownloadWorker(
FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p -> 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)) 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()){ if (finalPaths.isNotEmpty()){
setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id)) setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id))
}else{ }else{
finalPaths = listOf(context.getString(R.string.unfound_file)) finalPaths = mutableListOf(context.getString(R.string.unfound_file))
} }
}catch (e: Exception){ }catch (e: Exception){
finalPaths = listOf(context.getString(R.string.unfound_file)) finalPaths = mutableListOf(context.getString(R.string.unfound_file))
e.printStackTrace() e.printStackTrace()
handler.postDelayed({ handler.postDelayed({
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
@ -223,7 +234,7 @@ class DownloadWorker(
add("description") add("description")
add("txt") add("txt")
} }
finalPaths = finalPaths?.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } } finalPaths = finalPaths?.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } }?.toMutableList()
FileUtil.deleteConfigFiles(request) FileUtil.deleteConfigFiles(request)
//put download in history //put download in history

View file

@ -17,7 +17,7 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdl.database.repository.ResultRepository 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.FileUtil
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
@ -210,7 +210,7 @@ class ObserveSourceWorker(
.addTag("observeSources") .addTag("observeSources")
.addTag(sourceID.toString()) .addTag(sourceID.toString())
.setConstraints(workConstraints.build()) .setConstraints(workConstraints.build())
.setInitialDelay(System.currentTimeMillis() - item.calculateNextTime(), TimeUnit.MILLISECONDS) .setInitialDelay(item.calculateNextTimeForObserving() - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setInputData(Data.Builder().putLong("id", sourceID).build()) .setInputData(Data.Builder().putLong("id", sourceID).build())
WorkManager.getInstance(context).enqueueUniqueWork( WorkManager.getInstance(context).enqueueUniqueWork(

View file

@ -25,8 +25,10 @@ import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@ -140,29 +142,23 @@ class TerminalDownloadWorker(
} }
} }
return runBlocking { if (logDownloads) logRepo.update(it.out, logItem.id)
if (logDownloads) logRepo.update(it.out, logItem.id) dao.updateLog(it.out, itemId.toLong())
dao.updateLog(it.out, itemId.toLong()) notificationUtil.cancelDownloadNotification(itemId)
dao.delete(itemId.toLong()) delay(1000)
notificationUtil.cancelDownloadNotification(itemId) dao.delete(itemId.toLong())
Result.success()
Result.success()
}
}.onFailure { }.onFailure {
return runBlocking { if (it.message != null){
CoroutineScope(Dispatchers.IO).launch { if (logDownloads) logRepo.update(it.message!!, logItem.id)
if (it.message != null){ dao.updateLog(it.message!!, itemId.toLong())
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()
} }
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()
} }

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#db2e2e" android:pathData="M18.32,4.26C16.84,3.05 15.01,2.25 13,2.05v2.02c1.46,0.18 2.79,0.76 3.9,1.62L18.32,4.26zM19.93,11h2.02c-0.2,-2.01 -1,-3.84 -2.21,-5.32L18.31,7.1C19.17,8.21 19.75,9.54 19.93,11zM18.31,16.9l1.43,1.43c1.21,-1.48 2.01,-3.32 2.21,-5.32h-2.02C19.75,14.46 19.17,15.79 18.31,16.9zM13,19.93v2.02c2.01,-0.2 3.84,-1 5.32,-2.21l-1.43,-1.43C15.79,19.17 14.46,19.75 13,19.93zM13,12V7h-2v5H7l5,5l5,-5H13zM11,19.93v2.02c-5.05,-0.5 -9,-4.76 -9,-9.95s3.95,-9.45 9,-9.95v2.02C7.05,4.56 4,7.92 4,12S7.05,19.44 11,19.93z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="?android:colorAccent" android:pathData="M4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="?android:colorAccent" android:pathData="M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27 -7.38,5.74zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#db2e2e" android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z"/>
</vector>

View file

@ -0,0 +1,4 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?android:colorAccent" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M20,9H4v2h16V9zM4,15h16v-2H4V15z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#db2e2e" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="19dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="20dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M20,4H4C2.89,4 2,4.9 2,6v12c0,1.1 0.89,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.11,4 20,4zM20,18H4V8h16V18zM18,17h-6v-2h6V17zM7.5,17l-1.41,-1.41L8.67,13l-2.59,-2.59L7.5,9l4,4L7.5,17z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#db2e2e" android:pathData="M20,4H4C2.89,4 2,4.9 2,6v12c0,1.1 0.89,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.11,4 20,4zM20,18H4V8h16V18zM18,17h-6v-2h6V17zM7.5,17l-1.41,-1.41L8.67,13l-2.59,-2.59L7.5,9l4,4L7.5,17z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="19dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="20dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4z"/>
</vector>

View file

@ -166,23 +166,6 @@
app:layout_constraintVertical_bias="0.51" app:layout_constraintVertical_bias="0.51"
> >
<com.google.android.material.button.MaterialButton
android:id="@+id/active_download_pause"
style="?attr/materialIconButtonFilledStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/pause"
android:layoutAnimation="@anim/popup_enter"
app:backgroundTint="?attr/colorSurface"
app:cornerRadius="15dp"
app:icon="@drawable/exomedia_ic_play_arrow_white"
app:iconSize="30dp"
app:iconTint="?android:textColorPrimary"
app:layout_constraintBottom_toTopOf="@+id/output"
app:layout_constraintEnd_toStartOf="@id/active_download_delete"
app:layout_constraintTop_toBottomOf="@+id/linearlayout2_horizontalscrollview"
app:layout_constraintVertical_bias="0.51" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/active_download_delete" android:id="@+id/active_download_delete"
style="?attr/materialIconButtonFilledStyle" style="?attr/materialIconButtonFilledStyle"
@ -191,7 +174,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:backgroundTint="?attr/colorSurface" app:backgroundTint="?attr/colorSurface"
app:cornerRadius="15dp" app:cornerRadius="15dp"
app:icon="@drawable/baseline_delete_24" app:icon="@drawable/baseline_close_24"
app:iconSize="30dp" app:iconSize="30dp"
app:iconTint="?android:textColorPrimary" app:iconTint="?android:textColorPrimary"
app:layout_constraintBottom_toTopOf="@+id/output" app:layout_constraintBottom_toTopOf="@+id/output"

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView android:id="@+id/active_download_card_view" <com.google.android.material.card.MaterialCardView android:id="@+id/download_card_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:backgroundTint="@android:color/transparent"
android:checkable="true" android:checkable="true"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
@ -12,66 +13,53 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:cardCornerRadius="10dp"
android:layout_marginVertical="10dp"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
app:strokeWidth="0dp" app:strokeWidth="0dp"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="match_parent"
app:trackColor="#000"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:alpha="0.3"
android:scaleY="200"/>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:paddingHorizontal="10dp"
android:paddingVertical="10dp" android:paddingVertical="10dp"
android:layout_height="wrap_content"> android:layout_height="80dp">
<FrameLayout <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image_frame" android:id="@+id/downloads_image_view"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="50dp"
android:layout_marginStart="16dp"
android:adjustViewBounds="true"
android:background="?attr/colorSurfaceVariant"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@+id/download_item_data"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintDimensionRatio="H,16:9" app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" />
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
android:adjustViewBounds="true"
app:layout_constraintEnd_toStartOf="@+id/download_item_data"
app:layout_constraintBottom_toBottomOf="parent"
>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="match_parent"
android:elevation="10dp"
app:layout_constraintStart_toStartOf="@id/image_view"
app:layout_constraintEnd_toEndOf="@id/image_view"
app:layout_constraintBottom_toBottomOf="@id/image_view"
app:layout_constraintTop_toTopOf="@id/image_view"
android:layout_height="0dp"
android:layout_gravity="bottom"
android:alpha="0.6"
android:scaleY="200"
app:trackColor="#000" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintVertical_bias="0.0"
android:background="?attr/colorSurfaceVariant"
android:scaleType="centerCrop"/>
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/download_item_data" android:id="@+id/download_item_data"
app:layout_constraintVertical_bias="0.0"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/active_download_pause" app:layout_constraintEnd_toStartOf="@+id/options"
app:layout_constraintHorizontal_weight="0.7" app:layout_constraintStart_toEndOf="@+id/downloads_image_view"
app:layout_constraintStart_toEndOf="@+id/image_frame" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintVertical_bias="0.0">
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
@ -91,8 +79,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="bottom" android:gravity="bottom"
android:paddingStart="5dp"
android:paddingEnd="0dp"
android:scrollbars="none" android:scrollbars="none"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -100,74 +86,118 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title"> app:layout_constraintTop_toBottomOf="@+id/title">
<TextView <LinearLayout
android:id="@+id/format_note"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner" <TextView
android:backgroundTint="?attr/colorSecondary" android:id="@+id/download_type"
android:clickable="false" android:layout_width="wrap_content"
android:ellipsize="end" android:layout_height="wrap_content"
android:gravity="center" android:layout_marginHorizontal="5dp"
android:maxLength="17" android:background="@drawable/rounded_corner"
android:maxLines="1" android:backgroundTint="?attr/colorPrimaryContainer"
android:minWidth="30dp" android:clickable="false"
android:paddingHorizontal="5dp" android:gravity="center"
android:textSize="12sp" android:paddingHorizontal="5dp"
android:textStyle="bold" android:textColor="@color/white"
app:cornerRadius="10dp" android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent" android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent" app:cornerRadius="10dp"
app:layout_constraintTop_toTopOf="parent" /> app:drawableStartCompat="@drawable/ic_music_formatcard"
app:drawableTint="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/format_note"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/codec"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</HorizontalScrollView> </HorizontalScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/active_download_pause"
style="@style/Widget.Material3.Button.IconButton"
app:layout_constraintVertical_bias="0.0"
android:layout_width="wrap_content"
android:contentDescription="@string/pause"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
app:cornerRadius="15dp"
app:icon="@drawable/exomedia_ic_pause_white"
app:iconSize="30dp"
app:iconTint="?android:textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/active_download_delete"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/active_download_delete" android:id="@+id/options"
style="@style/Widget.Material3.Button.IconButton"
app:layout_constraintVertical_bias="0.0"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:contentDescription="@string/Remove"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground" android:layout_gravity="end|center_vertical"
app:cornerRadius="15dp" android:clickable="false"
app:icon="@drawable/baseline_delete_24" android:focusable="false"
app:iconSize="30dp" android:padding="16dp"
app:iconTint="?android:textColorPrimary" android:tintMode="src_in"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/baseline_more_vert_24"
app:tint="?attr/colorControlNormal"
<androidx.constraintlayout.widget.Barrier tools:ignore="ContentDescription" />
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="download_item_data,active_download_delete" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>

View file

@ -109,6 +109,44 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView
android:id="@+id/download_type"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/format_note" android:id="@+id/format_note"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary" style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
@ -150,24 +188,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout> </LinearLayout>

View file

@ -9,18 +9,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/pause_resume"
android:visibility="gone"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="30dp"
android:layout_gravity="end"
android:paddingVertical="10dp"
android:text="@string/resume"
android:textStyle="bold"/>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/download_recyclerview" android:id="@+id/download_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -30,12 +18,39 @@
android:scrollbars="none" android:scrollbars="none"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
android:paddingBottom="100dp"
app:layout_constraintTop_toBottomOf="@+id/pause_resume"> app:layout_constraintTop_toBottomOf="@+id/pause_resume">
</androidx.recyclerview.widget.RecyclerView> </androidx.recyclerview.widget.RecyclerView>
</LinearLayout> </LinearLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:text="@string/pause"
android:layout_margin="16dp"
android:contentDescription="@string/pause"
app:icon="@drawable/exomedia_ic_pause_white"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/resume"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:text="@string/resume"
android:layout_margin="16dp"
android:contentDescription="@string/resume"
app:icon="@drawable/exomedia_ic_play_arrow_white"
app:useCompatPadding="true" />
<include layout="@layout/no_results" <include layout="@layout/no_results"
android:visibility="gone"/> android:visibility="gone"
android:layout_width="match_parent"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -11,19 +11,36 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<TextView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/filesize" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:paddingHorizontal="30dp" <TextView
android:paddingVertical="10dp" android:id="@+id/filesize"
android:visibility="gone" android:layout_width="0dp"
android:text="@string/file_size" android:visibility="gone"
android:textStyle="bold" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" android:paddingHorizontal="30dp"
app:layout_constraintHorizontal_bias="0.0" android:paddingVertical="10dp"
app:layout_constraintStart_toStartOf="parent" android:text="@string/file_size"
app:layout_constraintTop_toTopOf="parent" /> android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/drag"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/drag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
app:drawableStartCompat="@drawable/ic_drag_handle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/download_recyclerview" android:id="@+id/download_recyclerview"

View file

@ -76,9 +76,9 @@
android:paddingEnd="10dp" android:paddingEnd="10dp"
android:scrollbars="none" android:scrollbars="none"
android:shadowColor="@color/black" android:shadowColor="@color/black"
android:shadowDx="4" android:shadowDx="2"
android:shadowDy="4" android:shadowDy="2"
android:shadowRadius="1.5" android:shadowRadius="1"
android:textColor="#FFF" android:textColor="#FFF"
android:textSize="11sp" android:textSize="11sp"
android:textStyle="bold" /> android:textStyle="bold" />

View file

@ -0,0 +1,251 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView android:id="@+id/download_card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@android:color/transparent"
android:checkable="true"
android:clickable="true"
android:focusable="true"
app:checkedIcon="@null"
app:strokeColor="?attr/colorPrimary"
app:cardPreventCornerOverlap="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
app:strokeWidth="0dp"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/drag_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:clickable="false"
android:visibility="gone"
android:focusable="false"
android:paddingVertical="16dp"
android:paddingEnd="20dp"
android:paddingStart="0dp"
android:tintMode="src_in"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_drag_handle"
app:tint="?attr/colorControlNormal"
tools:ignore="ContentDescription" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/card_content"
android:layout_width="0dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/drag_view"
app:layout_constraintEnd_toEndOf="parent"
android:layout_height="wrap_content">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/downloads_image_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:background="?attr/colorSurfaceVariant"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toStartOf="@+id/download_item_data"
app:layout_constraintHorizontal_weight="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" />
<TextView
android:id="@+id/duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="#80000000"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="@+id/downloads_image_view"
app:layout_constraintEnd_toEndOf="@+id/downloads_image_view" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/download_item_data"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="0.7"
app:layout_constraintStart_toEndOf="@+id/downloads_image_view"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:paddingHorizontal="5dp"
android:scrollbars="none"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/options"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="title,options" />
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="bottom"
android:paddingStart="5dp"
android:paddingEnd="0dp"
android:scrollbars="none"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/barrier">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/download_type"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/format_note"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/codec"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</HorizontalScrollView>
<com.google.android.material.button.MaterialButton
android:id="@+id/options"
style="?attr/materialIconButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="0dp"
android:padding="0dp"
app:cornerRadius="10dp"
app:icon="@drawable/baseline_more_vert_24"
app:iconTint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/scheduled_download_card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/scheduled_time"
android:paddingHorizontal="20dp"
android:paddingTop="10dp"
android:textStyle="bold"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<include layout="@layout/download_card" />
</LinearLayout>

View file

@ -66,6 +66,7 @@
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:paddingHorizontal="15dp" android:paddingHorizontal="15dp"
android:paddingBottom="15dp"
android:scrollbars="none" android:scrollbars="none"
android:layout_height="wrap_content"> android:layout_height="wrap_content">

View file

@ -0,0 +1,27 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity" >
<group>
<item
android:id="@+id/pause"
android:title="@string/pause"
android:icon="@drawable/exomedia_ic_pause_white"
app:showAsAction="ifRoom" />
<item
android:id="@+id/resume"
android:title="@string/resume"
android:icon="@drawable/exomedia_ic_play_arrow_white"
app:showAsAction="ifRoom" />
</group>
<item
android:id="@+id/cancel"
android:title="@string/cancel"
android:icon="@drawable/baseline_close_24"
app:showAsAction="ifRoom" />
</menu>

View file

@ -15,6 +15,12 @@
android:title="@string/clear_cancelled" android:title="@string/clear_cancelled"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/clear_scheduled"
android:icon="@drawable/ic_delete_all"
android:title="@string/clear_scheduled"
app:showAsAction="never" />
<item <item
android:id="@+id/clear_errored" android:id="@+id/clear_errored"

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<item
android:id="@+id/cancel"
android:title="@string/cancel" />
<item
android:id="@+id/move_top"
android:title="@string/move_top" />
<item
android:id="@+id/move_bottom"
android:title="@string/move_bottom" />
</group>
<item
android:id="@+id/copy_url"
android:icon="@drawable/ic_copy"
android:title="@string/copy_url" />
</menu>

View file

@ -20,7 +20,13 @@
android:id="@+id/up" android:id="@+id/up"
android:icon="@drawable/ic_arrow_up" android:icon="@drawable/ic_arrow_up"
app:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:title="Move Up" /> android:title="@string/move_top" />
<item
android:id="@+id/down"
android:icon="@drawable/ic_arrow_up"
app:showAsAction="ifRoom"
android:title="@string/move_bottom" />
<item <item
android:id="@+id/download" android:id="@+id/download"

View file

@ -353,8 +353,7 @@
<string name="month">شهر</string> <string name="month">شهر</string>
<string name="bitrate">معدّل البتات</string> <string name="bitrate">معدّل البتات</string>
<string name="not_deleted">غير المحذوفة</string> <string name="not_deleted">غير المحذوفة</string>
<string name="download_archive_summary">استخدام ملف أرشيف التنزيل الخاص بـ yt-dlp لمنع التنزيل المكرر بدلًا من الطريقة الافتراضية</string> <string name="archive"> أرشيف التنزيل [استخدام ملف أرشيف التنزيل الخاص بـ yt-dlp لمنع التنزيل المكرر بدلًا من الطريقة الافتراضية]</string>
<string name="download_archive">أرشيف التنزيل</string>
<string name="on_time">في</string> <string name="on_time">في</string>
<string name="retry_missing_downloads">إعادة المحاولة للتنزيلات المفقودة</string> <string name="retry_missing_downloads">إعادة المحاولة للتنزيلات المفقودة</string>
<string name="save_auto_subs">حفظ الترجمات التلقائية</string> <string name="save_auto_subs">حفظ الترجمات التلقائية</string>

View file

@ -351,14 +351,13 @@
<string name="bitrate">Bit Sürəti</string> <string name="bitrate">Bit Sürəti</string>
<string name="after">Sonra</string> <string name="after">Sonra</string>
<string name="not_deleted">Silinməyib</string> <string name="not_deleted">Silinməyib</string>
<string name="download_archive">Yükləmə Arxivi</string> <string name="archive">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]</string>
<string name="on_time"></string> <string name="on_time"></string>
<string name="observe_sources">Mənbələrə Nəzarət Et</string> <string name="observe_sources">Mənbələrə Nəzarət Et</string>
<string name="confirm_delete_sources_desc">Bütün mənbələri həmişəlik sil</string> <string name="confirm_delete_sources_desc">Bütün mənbələri həmişəlik sil</string>
<string name="starts">Başlayır</string> <string name="starts">Başlayır</string>
<string name="occurrences">proses</string> <string name="occurrences">proses</string>
<string name="save_auto_subs">Avtomatik Titirləri Saxla</string> <string name="save_auto_subs">Avtomatik Titirləri Saxla</string>
<string name="download_archive_summary">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</string>
<string name="text_size">Mətn Ölçüsü</string> <string name="text_size">Mətn Ölçüsü</string>
<string name="add">Əlavə et</string> <string name="add">Əlavə et</string>
<string name="swipe_gestures_download_card">Yükləmə Kartında Sürüşdürmə Jestləri</string> <string name="swipe_gestures_download_card">Yükləmə Kartında Sürüşdürmə Jestləri</string>

View file

@ -350,7 +350,7 @@
<string name="retry_missing_downloads">Opakovat zmeškané stahování</string> <string name="retry_missing_downloads">Opakovat zmeškané stahování</string>
<string name="save_auto_subs">Uložit automatické titulky</string> <string name="save_auto_subs">Uložit automatické titulky</string>
<string name="not_deleted">Neodstraněno</string> <string name="not_deleted">Neodstraněno</string>
<string name="download_archive">Stáhnout archiv</string> <string name="archive">Stáhnout archiv [Použije archivní soubor yt-dlp pro zabránění duplicitního stahování místo vestavěné funkce]</string>
<string name="observe_sources">Sledovat zdroje</string> <string name="observe_sources">Sledovat zdroje</string>
<string name="new_source">Nový zfroj</string> <string name="new_source">Nový zfroj</string>
<string name="confirm_delete_sources_desc">Trvale odstraní všechny zdroje</string> <string name="confirm_delete_sources_desc">Trvale odstraní všechny zdroje</string>
@ -358,7 +358,6 @@
<string name="on_time">Zapnout</string> <string name="on_time">Zapnout</string>
<string name="month">Měsíc</string> <string name="month">Měsíc</string>
<string name="bitrate">Rychlost přenosu</string> <string name="bitrate">Rychlost přenosu</string>
<string name="download_archive_summary">Použije archivní soubor yt-dlp pro zabránění duplicitního stahování místo vestavěné funkce</string>
<string name="show_download_count">Zobrazení počtů na obrazovce stahovací fronty</string> <string name="show_download_count">Zobrazení počtů na obrazovce stahovací fronty</string>
<string name="thumbnail_format">Formát miniatur</string> <string name="thumbnail_format">Formát miniatur</string>
<string name="get_new_uploads">Získat pouze nové nahrávky</string> <string name="get_new_uploads">Získat pouze nové nahrávky</string>

View file

@ -353,8 +353,7 @@
<string name="bitrate">Tasa de datos</string> <string name="bitrate">Tasa de datos</string>
<string name="save_auto_subs">Guardar subtítulos automáticos</string> <string name="save_auto_subs">Guardar subtítulos automáticos</string>
<string name="not_deleted">No eliminado</string> <string name="not_deleted">No eliminado</string>
<string name="download_archive">Descargar archivo</string> <string name="archive">Descargar archivo [Utilice el archivo de descarga de yt-dlp para evitar descargas duplicadas en lugar de la funcionalidad integrada]</string>
<string name="download_archive_summary">Utilice el archivo de descarga de yt-dlp para evitar descargas duplicadas en lugar de la funcionalidad integrada</string>
<string name="on_time">En</string> <string name="on_time">En</string>
<string name="week">Semana</string> <string name="week">Semana</string>
<string name="confirm_delete_sources_desc">Borrar todas las fuentes, permanentemente</string> <string name="confirm_delete_sources_desc">Borrar todas las fuentes, permanentemente</string>

View file

@ -350,14 +350,13 @@
<string name="bitrate">बिटरेट</string> <string name="bitrate">बिटरेट</string>
<string name="save_auto_subs">स्वचालित उपशीर्षक सहेजें</string> <string name="save_auto_subs">स्वचालित उपशीर्षक सहेजें</string>
<string name="not_deleted">हटाया नहीं गया</string> <string name="not_deleted">हटाया नहीं गया</string>
<string name="download_archive">डाउनलोड संग्रह</string> <string name="archive">डाउनलोड संग्रह [डुप्लिकेट डाउनलोड को रोकने के लिए अंतर्निहित कार्यक्षमता के बजाय yt-dlp की डाउनलोड संग्रह फ़ाइल का उपयोग करें]</string>
<string name="on_time">नियत समय पर</string> <string name="on_time">नियत समय पर</string>
<string name="delete_all_sources">सभी स्रोत हटाएँ</string> <string name="delete_all_sources">सभी स्रोत हटाएँ</string>
<string name="force_ipv4">IPv4 को बाध्य करें</string> <string name="force_ipv4">IPv4 को बाध्य करें</string>
<string name="never">कभी नहीं</string> <string name="never">कभी नहीं</string>
<string name="copy_urls">URL कॉपी करें</string> <string name="copy_urls">URL कॉपी करें</string>
<string name="occurrences">घटनाएँ</string> <string name="occurrences">घटनाएँ</string>
<string name="download_archive_summary">डुप्लिकेट डाउनलोड को रोकने के लिए अंतर्निहित कार्यक्षमता के बजाय yt-dlp की डाउनलोड संग्रह फ़ाइल का उपयोग करें</string>
<string name="retry_missing_downloads">गुम हुए डाउनलोड पुनः प्रयास करें</string> <string name="retry_missing_downloads">गुम हुए डाउनलोड पुनः प्रयास करें</string>
<string name="thumbnail_format">थंबनेल प्रारूप</string> <string name="thumbnail_format">थंबनेल प्रारूप</string>
<string name="get_new_uploads">केवल नए अपलोड प्राप्त करें</string> <string name="get_new_uploads">केवल नए अपलोड प्राप्त करें</string>

View file

@ -352,8 +352,7 @@
<string name="bitrate">Bitráta</string> <string name="bitrate">Bitráta</string>
<string name="save_auto_subs">Feliratok automatikus mentése</string> <string name="save_auto_subs">Feliratok automatikus mentése</string>
<string name="not_deleted">Nincs törölve</string> <string name="not_deleted">Nincs törölve</string>
<string name="download_archive">Archívum letöltése</string> <string name="archive">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]</string>
<string name="download_archive_summary">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</string>
<string name="on_time">Be</string> <string name="on_time">Be</string>
<string name="occurrences">események</string> <string name="occurrences">események</string>
<string name="day">Nap</string> <string name="day">Nap</string>

View file

@ -342,10 +342,9 @@
<string name="retry_missing_downloads">Coba Kembali Unduhan yang Hilang</string> <string name="retry_missing_downloads">Coba Kembali Unduhan yang Hilang</string>
<string name="bitrate">Laju Bit</string> <string name="bitrate">Laju Bit</string>
<string name="save_auto_subs">Simpan Takarir (Subtitle) Otomatis</string> <string name="save_auto_subs">Simpan Takarir (Subtitle) Otomatis</string>
<string name="download_archive">Arsip Unduhan</string> <string name="archive">Arsip Unduhan [Gunakan berkas arsip unduhan yt-dlp untuk mencegah unduhan duplikat alih-alih menggunakan fungsionalitas bawaan]</string>
<string name="observe_sources">Amati Sumber</string> <string name="observe_sources">Amati Sumber</string>
<string name="confirm_delete_sources_desc">Hapus semua sumber, secara permanen</string> <string name="confirm_delete_sources_desc">Hapus semua sumber, secara permanen</string>
<string name="download_archive_summary">Gunakan berkas arsip unduhan yt-dlp untuk mencegah unduhan duplikat alih-alih menggunakan fungsionalitas bawaan</string>
<string name="day">Hari</string> <string name="day">Hari</string>
<string name="not_deleted">Tidak Dihapus</string> <string name="not_deleted">Tidak Dihapus</string>
<string name="changelog">Log Perubahan</string> <string name="changelog">Log Perubahan</string>

View file

@ -351,7 +351,7 @@
<string name="bitrate">Velocità di trasmissione</string> <string name="bitrate">Velocità di trasmissione</string>
<string name="save_auto_subs">Salva i Sottotitoli Automatici</string> <string name="save_auto_subs">Salva i Sottotitoli Automatici</string>
<string name="not_deleted">Non Eliminato</string> <string name="not_deleted">Non Eliminato</string>
<string name="download_archive">Scarica Archivio</string> <string name="archive">Scarica Archivio [Utilizza l\'archivio download di yt-dlp per evitare download duplicati invece della funzionalità integrata]</string>
<string name="on_time">Acceso</string> <string name="on_time">Acceso</string>
<string name="no_download_fragments">Non scaricare come Frammenti</string> <string name="no_download_fragments">Non scaricare come Frammenti</string>
<string name="no_download_fragments_summary">Non usare i file .part e scrivi direttamente nel file di output</string> <string name="no_download_fragments_summary">Non usare i file .part e scrivi direttamente nel file di output</string>
@ -366,7 +366,6 @@
<string name="confirm_delete_sources_desc">Cancella tutti i sorgenti, permanentemente</string> <string name="confirm_delete_sources_desc">Cancella tutti i sorgenti, permanentemente</string>
<string name="copy_urls">Copia URLs</string> <string name="copy_urls">Copia URLs</string>
<string name="never">Mai</string> <string name="never">Mai</string>
<string name="download_archive_summary">Utilizza l\'archivio download di yt-dlp per evitare download duplicati invece della funzionalità integrata</string>
<string name="swipe_gestures_download_card">Gestualità di Scorrimento nella download card</string> <string name="swipe_gestures_download_card">Gestualità di Scorrimento nella download card</string>
<string name="get_new_uploads">Ricevi Solo Nuovi Upload</string> <string name="get_new_uploads">Ricevi Solo Nuovi Upload</string>
<string name="also_download_audio">Scarica Anche come Audio</string> <string name="also_download_audio">Scarica Anche come Audio</string>

View file

@ -345,9 +345,8 @@
<string name="week"></string> <string name="week"></string>
<string name="retry_missing_downloads">ダウンロード失敗時に再試行</string> <string name="retry_missing_downloads">ダウンロード失敗時に再試行</string>
<string name="not_deleted">未削除</string> <string name="not_deleted">未削除</string>
<string name="download_archive">ダウンロード記録</string> <string name="archive">ダウンロード記録 [内蔵の機能ではなく yt-dlp のダウンロード記録ファイルを使い重複ダウンロードを防止]</string>
<string name="save_auto_subs">自動字幕を保存</string> <string name="save_auto_subs">自動字幕を保存</string>
<string name="download_archive_summary">内蔵の機能ではなく yt-dlp のダウンロード記録ファイルを使い重複ダウンロードを防止</string>
<string name="force_ipv4_desc">すべての接続を IPv4 で行う</string> <string name="force_ipv4_desc">すべての接続を IPv4 で行う</string>
<string name="observe_sources">指定先を監視</string> <string name="observe_sources">指定先を監視</string>
<string name="new_source">新しい監視先</string> <string name="new_source">新しい監視先</string>

View file

@ -337,8 +337,7 @@
<string name="retry_missing_downloads">누락된 다운로드 재시도</string> <string name="retry_missing_downloads">누락된 다운로드 재시도</string>
<string name="save_auto_subs">자동 자막 저장</string> <string name="save_auto_subs">자동 자막 저장</string>
<string name="not_deleted">삭제되지 않음</string> <string name="not_deleted">삭제되지 않음</string>
<string name="download_archive">아카이브 다운로드</string> <string name="archive">아카이브 다운로드 [내장된 기능 대신 yt-dlp의 다운로드 아카이브를 사용하여 중복된 다운로드를 방지합니다]</string>
<string name="download_archive_summary">내장된 기능 대신 yt-dlp의 다운로드 아카이브를 사용하여 중복된 다운로드를 방지합니다</string>
<string name="preferred_audio_language">선호하는 오디오 언어</string> <string name="preferred_audio_language">선호하는 오디오 언어</string>
<string name="changed_path_for_everyone_to">모든 항목의 다운로드 경로를 바꾸기:</string> <string name="changed_path_for_everyone_to">모든 항목의 다운로드 경로를 바꾸기:</string>
<string name="dismiss">무시</string> <string name="dismiss">무시</string>

View file

@ -349,7 +349,7 @@
<string name="retry_missing_downloads">ਗੁੰਮ ਹੋਏ ਡਾਉਨਲੋਡਸ ਲਈ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ</string> <string name="retry_missing_downloads">ਗੁੰਮ ਹੋਏ ਡਾਉਨਲੋਡਸ ਲਈ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ</string>
<string name="bitrate">ਬਿੱਟਰੇਟ</string> <string name="bitrate">ਬਿੱਟਰੇਟ</string>
<string name="not_deleted">ਮਿਟਾਇਆ ਨਹੀਂ ਗਿਆ</string> <string name="not_deleted">ਮਿਟਾਇਆ ਨਹੀਂ ਗਿਆ</string>
<string name="download_archive">ਡਾਊਨਲੋਡ ਆਰਕਾਈਵ</string> <string name="archive">ਡਾਊਨਲੋਡ ਆਰਕਾਈਵ [ਬਿਲਟ-ਇਨ ਫੰਕਸ਼ਨੈਲਿਟੀ ਦੀ ਬਜਾਏ ਡੁਪਲੀਕੇਟ ਡਾਉਨਲੋਡਸ ਨੂੰ ਰੋਕਣ ਲਈ yt-dlp ਦੀ ਡਾਉਨਲੋਡ ਆਰਕਾਈਵ ਫਾਈਲ ਦੀ ਵਰਤੋਂ ਕਰੋ]</string>
<string name="on_time">\'ਤੇ</string> <string name="on_time">\'ਤੇ</string>
<string name="observe_sources">ਸਰੋਤਾਂ ਦੀ ਨਿਗਰਾਨੀ ਕਰੋ</string> <string name="observe_sources">ਸਰੋਤਾਂ ਦੀ ਨਿਗਰਾਨੀ ਕਰੋ</string>
<string name="confirm_delete_sources_desc">ਸਾਰੇ ਸਰੋਤਾਂ ਨੂੰ ਪੱਕੇ ਤੌਰ \'ਤੇ ਮਿਟਾਓ</string> <string name="confirm_delete_sources_desc">ਸਾਰੇ ਸਰੋਤਾਂ ਨੂੰ ਪੱਕੇ ਤੌਰ \'ਤੇ ਮਿਟਾਓ</string>
@ -358,7 +358,6 @@
<string name="never">ਕਦੇ ਨਹੀਂ</string> <string name="never">ਕਦੇ ਨਹੀਂ</string>
<string name="ends">ਖਤਮ ਹੁੰਦਾ ਹੈ</string> <string name="ends">ਖਤਮ ਹੁੰਦਾ ਹੈ</string>
<string name="save_auto_subs">ਸਵੈਚਲਿਤ ਸਬਟਾਈਟਲਾਂ ਨੂੰ ਸੇਵ ਕਰੋ</string> <string name="save_auto_subs">ਸਵੈਚਲਿਤ ਸਬਟਾਈਟਲਾਂ ਨੂੰ ਸੇਵ ਕਰੋ</string>
<string name="download_archive_summary">ਬਿਲਟ-ਇਨ ਫੰਕਸ਼ਨੈਲਿਟੀ ਦੀ ਬਜਾਏ ਡੁਪਲੀਕੇਟ ਡਾਉਨਲੋਡਸ ਨੂੰ ਰੋਕਣ ਲਈ yt-dlp ਦੀ ਡਾਉਨਲੋਡ ਆਰਕਾਈਵ ਫਾਈਲ ਦੀ ਵਰਤੋਂ ਕਰੋ</string>
<string name="swipe_gestures_download_card">ਡਾਉਨਲੋਡ ਕਾਰਡ ਵਿੱਚ ਸਵਾਈਪ ਇਸ਼ਾਰੇ</string> <string name="swipe_gestures_download_card">ਡਾਉਨਲੋਡ ਕਾਰਡ ਵਿੱਚ ਸਵਾਈਪ ਇਸ਼ਾਰੇ</string>
<string name="thumbnail_format">ਥੰਮਨੇਲ ਫਾਰਮੈਟ</string> <string name="thumbnail_format">ਥੰਮਨੇਲ ਫਾਰਮੈਟ</string>
<string name="text_size">ਟੈਕਸਟ ਦਾ ਆਕਾਰ</string> <string name="text_size">ਟੈਕਸਟ ਦਾ ਆਕਾਰ</string>

View file

@ -354,8 +354,7 @@
<string name="set_time">Ustaw czas</string> <string name="set_time">Ustaw czas</string>
<string name="starts">Uruchom</string> <string name="starts">Uruchom</string>
<string name="retry_missing_downloads">Spróbuj pobrać ponownie</string> <string name="retry_missing_downloads">Spróbuj pobrać ponownie</string>
<string name="download_archive">Pobierz archiwum</string> <string name="archive">Pobierz archiwum [Użyj pliku archiwum pobierania yt-dlp, aby zapobiec duplikatom pobierania zamiast wbudowanej funkcjonalności]</string>
<string name="download_archive_summary">Użyj pliku archiwum pobierania yt-dlp, aby zapobiec duplikatom pobierania zamiast wbudowanej funkcjonalności</string>
<string name="month">Miesiąc</string> <string name="month">Miesiąc</string>
<string name="not_deleted">Nie usunięte</string> <string name="not_deleted">Nie usunięte</string>
<string name="save_auto_subs">Zapisz automatyczne napisy</string> <string name="save_auto_subs">Zapisz automatyczne napisy</string>

View file

@ -347,7 +347,7 @@
<string name="month">Mês(es)</string> <string name="month">Mês(es)</string>
<string name="bitrate">Taxa de bits</string> <string name="bitrate">Taxa de bits</string>
<string name="not_deleted">Não excluído</string> <string name="not_deleted">Não excluído</string>
<string name="download_archive">Arquivar downloads</string> <string name="archive">Arquivar downloads [Usar o arquivamento de downloads do yt-dlp para evitar downloads duplicados]</string>
<string name="observe_sources">Observar Origens</string> <string name="observe_sources">Observar Origens</string>
<string name="delete_all_sources">Excluir todas as origens</string> <string name="delete_all_sources">Excluir todas as origens</string>
<string name="confirm_delete_sources_desc">Excluir todas as origens permanentemente</string> <string name="confirm_delete_sources_desc">Excluir todas as origens permanentemente</string>
@ -356,7 +356,6 @@
<string name="occurrences">ocorrências</string> <string name="occurrences">ocorrências</string>
<string name="day">Dia(s)</string> <string name="day">Dia(s)</string>
<string name="save_auto_subs">Salvar legendas automáticas</string> <string name="save_auto_subs">Salvar legendas automáticas</string>
<string name="download_archive_summary">Usar o arquivamento de downloads do yt-dlp para evitar downloads duplicados</string>
<string name="retry_missing_downloads">Repetir downloads ausentes</string> <string name="retry_missing_downloads">Repetir downloads ausentes</string>
<string name="force_ipv4_desc">Efetuar todas as conexões com IPv4</string> <string name="force_ipv4_desc">Efetuar todas as conexões com IPv4</string>
<string name="thumbnail_format">Formato da miniatura</string> <string name="thumbnail_format">Formato da miniatura</string>

View file

@ -357,8 +357,7 @@
<string name="month">Mês</string> <string name="month">Mês</string>
<string name="set_time">Definir tempo</string> <string name="set_time">Definir tempo</string>
<string name="on_time">Em</string> <string name="on_time">Em</string>
<string name="download_archive">Arquivo de descarga</string> <string name="archive">Arquivo de descarga [Utilizar ficheiro de descarga yt-dlp para evitar duplicação de descargas]</string>
<string name="download_archive_summary">Utilizar ficheiro de descarga yt-dlp para evitar duplicação de descargas</string>
<string name="swipe_gestures_download_card">Gestos de deslizar no cartão de descarga</string> <string name="swipe_gestures_download_card">Gestos de deslizar no cartão de descarga</string>
<string name="show_download_count">Mostrar contagens no ecrã da Fila de descarga</string> <string name="show_download_count">Mostrar contagens no ecrã da Fila de descarga</string>
<string name="get_new_uploads">Obter apenas novos envios</string> <string name="get_new_uploads">Obter apenas novos envios</string>

View file

@ -346,7 +346,6 @@
<string name="on_time">Вкл</string> <string name="on_time">Вкл</string>
<string name="day">День</string> <string name="day">День</string>
<string name="occurrences">случаев</string> <string name="occurrences">случаев</string>
<string name="download_archive_summary">Используйте архивный файл загрузок yt-dlp, чтобы предотвратить дублирование загрузок вместо встроенной функции</string>
<string name="observe_sources">Источники загрузки</string> <string name="observe_sources">Источники загрузки</string>
<string name="new_source">Новый источник</string> <string name="new_source">Новый источник</string>
<string name="confirm_delete_sources_desc">Удалить все источники навсегда</string> <string name="confirm_delete_sources_desc">Удалить все источники навсегда</string>
@ -358,7 +357,7 @@
<string name="bitrate">Битрейт</string> <string name="bitrate">Битрейт</string>
<string name="save_auto_subs">Сохранить автоматически субтитры</string> <string name="save_auto_subs">Сохранить автоматически субтитры</string>
<string name="not_deleted">Не удалено</string> <string name="not_deleted">Не удалено</string>
<string name="download_archive">Скачать архив</string> <string name="archive">Скачать архив [Используйте архивный файл загрузок yt-dlp, чтобы предотвратить дублирование загрузок вместо встроенной функции]</string>
<string name="swipe_gestures_download_card">Жесты смахивания в карточке загрузок</string> <string name="swipe_gestures_download_card">Жесты смахивания в карточке загрузок</string>
<string name="show_download_count">Показывать кол-во контента на экране очереди загрузки</string> <string name="show_download_count">Показывать кол-во контента на экране очереди загрузки</string>
<string name="thumbnail_format">Формат миниатюр</string> <string name="thumbnail_format">Формат миниатюр</string>

View file

@ -345,8 +345,7 @@
<string name="retry_missing_downloads">Opakovať zmeškané sťahovania</string> <string name="retry_missing_downloads">Opakovať zmeškané sťahovania</string>
<string name="save_auto_subs">Uložiť automatické titulky</string> <string name="save_auto_subs">Uložiť automatické titulky</string>
<string name="not_deleted">Nevymazané</string> <string name="not_deleted">Nevymazané</string>
<string name="download_archive">Stiahnuť archív</string> <string name="archive">Stiahnuť archív [Použije archívny súbor yt-dlp namiesto vstavanej funkcie, aby sa zabránilo duplicitnému sťahovaniu]</string>
<string name="download_archive_summary">Použije archívny súbor yt-dlp namiesto vstavanej funkcie, aby sa zabránilo duplicitnému sťahovaniu</string>
<string name="copy_urls">Kopírovať adresy URL</string> <string name="copy_urls">Kopírovať adresy URL</string>
<string name="observe_sources">Sledovať zdroje</string> <string name="observe_sources">Sledovať zdroje</string>
<string name="new_source">Nový zdroj</string> <string name="new_source">Nový zdroj</string>

View file

@ -357,8 +357,7 @@
<string name="ends">Mbaron</string> <string name="ends">Mbaron</string>
<string name="day">Ditë</string> <string name="day">Ditë</string>
<string name="month">Muaj</string> <string name="month">Muaj</string>
<string name="download_archive">Arkiva e Shkarkimeve</string> <string name="archive">Arkiva e Shkarkimeve [Përdor skedarin e arkivës të yt-dlp për të parandaluar shkarkimet duplicate]</string>
<string name="download_archive_summary">Përdor skedarin e arkivës të yt-dlp për të parandaluar shkarkimet duplicate në vend të funksionalitetit të aplikacionit</string>
<string name="swipe_gestures_download_card">Gjestet e rrëshqitjes në kartën e shkarkimit</string> <string name="swipe_gestures_download_card">Gjestet e rrëshqitjes në kartën e shkarkimit</string>
<string name="show_download_count">Shfaq numrat në faqen e rradhës së shkarkimeve</string> <string name="show_download_count">Shfaq numrat në faqen e rradhës së shkarkimeve</string>
<string name="also_download_audio">Shkarko Gjithashtu si Audio</string> <string name="also_download_audio">Shkarko Gjithashtu si Audio</string>

View file

@ -357,8 +357,7 @@
<string name="every">Сваки</string> <string name="every">Сваки</string>
<string name="after">Након</string> <string name="after">Након</string>
<string name="not_deleted">Није избрисано</string> <string name="not_deleted">Није избрисано</string>
<string name="download_archive_summary">Користите yt-dlp архиву преузимања да бисте спречили дупла преузимања, уместо уграђене функционалности</string> <string name="archive">Архива преузимања [Користите yt-dlp архиву преузимања да бисте спречили дупла преузимања, уместо уграђене функционалности]</string>
<string name="download_archive">Архива преузимања</string>
<string name="show_download_count">Прикажи бројеве на екрану редоследа за преузимање</string> <string name="show_download_count">Прикажи бројеве на екрану редоследа за преузимање</string>
<string name="get_new_uploads">Добијај само нова отпремања</string> <string name="get_new_uploads">Добијај само нова отпремања</string>
<string name="also_download_audio">Такође преузми као аудио снимак</string> <string name="also_download_audio">Такође преузми као аудио снимак</string>

View file

@ -356,12 +356,11 @@
<string name="bitrate">Bit hızı</string> <string name="bitrate">Bit hızı</string>
<string name="save_auto_subs">Otomatik Altyazıları Kaydet</string> <string name="save_auto_subs">Otomatik Altyazıları Kaydet</string>
<string name="not_deleted">Silinmedi</string> <string name="not_deleted">Silinmedi</string>
<string name="download_archive_summary">Yerleşik işlevsellik yerine yinelenen indirmeleri önlemek için yt-dlp\'nin indirme arşiv dosyasını kullanın</string>
<string name="every">Herhangi</string> <string name="every">Herhangi</string>
<string name="never">Asla</string> <string name="never">Asla</string>
<string name="on_time">Çalışmakta</string> <string name="on_time">Çalışmakta</string>
<string name="day">Gün</string> <string name="day">Gün</string>
<string name="download_archive">İndirme Arşivi</string> <string name="archive">İndirme Arşivi [Yerleşik işlevsellik yerine yinelenen indirmeleri önlemek için yt-dlp\'nin indirme arşiv dosyasını kullanın]</string>
<string name="thumbnail_format">Küçük Resim Formatı</string> <string name="thumbnail_format">Küçük Resim Formatı</string>
<string name="get_new_uploads">Yalnızca Yeni Yüklemeleri Alın</string> <string name="get_new_uploads">Yalnızca Yeni Yüklemeleri Alın</string>
<string name="also_download_audio">Ayrıca Ses Olarak İndir</string> <string name="also_download_audio">Ayrıca Ses Olarak İndir</string>

View file

@ -357,8 +357,7 @@
<string name="month">Місяць</string> <string name="month">Місяць</string>
<string name="not_deleted">Не видалено</string> <string name="not_deleted">Не видалено</string>
<string name="save_auto_subs">Зберегти автоматично субтитри</string> <string name="save_auto_subs">Зберегти автоматично субтитри</string>
<string name="download_archive">Архів завантажень</string> <string name="archive">Архів завантажень [Використовуйте файл архіву завантажень yt-dlp, щоб запобігти повторним завантаженням замість вбудованої функції]</string>
<string name="download_archive_summary">Використовуйте файл архіву завантажень yt-dlp, щоб запобігти повторним завантаженням замість вбудованої функції</string>
<string name="swipe_gestures_download_card">Жести пальцем у картці завантаження</string> <string name="swipe_gestures_download_card">Жести пальцем у картці завантаження</string>
<string name="show_download_count">Показати підрахунки на екрані черги завантаження</string> <string name="show_download_count">Показати підрахунки на екрані черги завантаження</string>
<string name="thumbnail_format">Формат мініатюр</string> <string name="thumbnail_format">Формат мініатюр</string>

View file

@ -336,8 +336,7 @@
<string name="force_ipv4_desc">Thực hiện tất cả các kết nối qua IPv4</string> <string name="force_ipv4_desc">Thực hiện tất cả các kết nối qua IPv4</string>
<string name="copy_urls">Sao chép URL</string> <string name="copy_urls">Sao chép URL</string>
<string name="save_auto_subs">Lưu phụ đề tự động</string> <string name="save_auto_subs">Lưu phụ đề tự động</string>
<string name="download_archive_summary">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</string> <string name="archive">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]</string>
<string name="download_archive">Tải xuống lưu trữ</string>
<string name="dismiss">Bỏ qua</string> <string name="dismiss">Bỏ qua</string>
<string name="changed_path_for_everyone_to">Đã thay đổi đường dẫn tải xuống của mọi mục thành:</string> <string name="changed_path_for_everyone_to">Đã thay đổi đường dẫn tải xuống của mọi mục thành:</string>
<string name="all">Tất cả</string> <string name="all">Tất cả</string>

View file

@ -352,13 +352,12 @@
<string name="ends">结束</string> <string name="ends">结束</string>
<string name="after"></string> <string name="after"></string>
<string name="day"></string> <string name="day"></string>
<string name="download_archive_summary">使用 yt-dlp 的下载存档文件而非应用的内置功能来防止重复下载</string>
<string name="month"></string> <string name="month"></string>
<string name="retry_missing_downloads">重试缺少的下载</string> <string name="retry_missing_downloads">重试缺少的下载</string>
<string name="not_deleted">未删除</string> <string name="not_deleted">未删除</string>
<string name="bitrate">比特率</string> <string name="bitrate">比特率</string>
<string name="save_auto_subs">保存自动字幕</string> <string name="save_auto_subs">保存自动字幕</string>
<string name="download_archive">下载存档</string> <string name="archive">下载存档 [使用 yt-dlp 的下载存档文件而非应用的内置功能来防止重复下载]</string>
<string name="also_download_audio">同样下载为音频</string> <string name="also_download_audio">同样下载为音频</string>
<string name="wrap_text">换行显示过长文本</string> <string name="wrap_text">换行显示过长文本</string>
<string name="text_size">文本尺寸</string> <string name="text_size">文本尺寸</string>

View file

@ -348,7 +348,7 @@
<string name="never">從不</string> <string name="never">從不</string>
<string name="after">之後</string> <string name="after">之後</string>
<string name="occurrences">次數</string> <string name="occurrences">次數</string>
<string name="download_archive">下載存檔</string> <string name="archive">下載存檔 [使用 yt-dlp 的下載存檔檔案以防止重複下載,而非使用內建功能]</string>
<string name="not_deleted">未刪除</string> <string name="not_deleted">未刪除</string>
<string name="save_auto_subs">儲存自動字幕</string> <string name="save_auto_subs">儲存自動字幕</string>
<string name="day"></string> <string name="day"></string>
@ -356,7 +356,6 @@
<string name="month"></string> <string name="month"></string>
<string name="retry_missing_downloads">重試遺漏的下載</string> <string name="retry_missing_downloads">重試遺漏的下載</string>
<string name="bitrate">比特率</string> <string name="bitrate">比特率</string>
<string name="download_archive_summary">使用 yt-dlp 的下載存檔檔案以防止重複下載,而非使用內建功能</string>
<string name="on_time"></string> <string name="on_time"></string>
<string name="force_ipv4_desc">使所有連線均使用 IPv4</string> <string name="force_ipv4_desc">使所有連線均使用 IPv4</string>
</resources> </resources>

View file

@ -25,14 +25,18 @@
<item>@string/defaultValue</item> <item>@string/defaultValue</item>
<item>mp4</item> <item>mp4</item>
<item>mkv</item> <item>mkv</item>
<item>webm</item> <item>avi</item>
<item>flv</item>
<item>mov</item>
</string-array> </string-array>
<string-array name="video_containers_values"> <string-array name="video_containers_values">
<item>Default</item> <item>Default</item>
<item>mp4</item> <item>mp4</item>
<item>mkv</item> <item>mkv</item>
<item>webm</item> <item>avi</item>
<item>flv</item>
<item>mov</item>
</string-array> </string-array>
<string-array name="video_formats"> <string-array name="video_formats">
@ -116,6 +120,7 @@
<item>Azərbaycanca</item> <item>Azərbaycanca</item>
<item>বাংলা</item> <item>বাংলা</item>
<item>বাংলা (India)</item> <item>বাংলা (India)</item>
<item>български</item>
<item>Босански</item> <item>Босански</item>
<item>Deutsch</item> <item>Deutsch</item>
<item>Ἑλληνική</item> <item>Ἑλληνική</item>
@ -159,6 +164,7 @@
<item>az</item> <item>az</item>
<item>bn</item> <item>bn</item>
<item>bn-IN</item> <item>bn-IN</item>
<item>bg</item>
<item>bs</item> <item>bs</item>
<item>de</item> <item>de</item>
<item>el</item> <item>el</item>
@ -1084,6 +1090,7 @@
<item>@string/downloads</item> <item>@string/downloads</item>
<item>@string/in_queue</item> <item>@string/in_queue</item>
<item>@string/cancelled</item> <item>@string/cancelled</item>
<item>@string/scheduled</item>
<item>@string/errored</item> <item>@string/errored</item>
<item>@string/saved</item> <item>@string/saved</item>
<item>@string/command_templates</item> <item>@string/command_templates</item>
@ -1095,6 +1102,7 @@
<item>history</item> <item>history</item>
<item>queued</item> <item>queued</item>
<item>cancelled</item> <item>cancelled</item>
<item>scheduled</item>
<item>errored</item> <item>errored</item>
<item>saved</item> <item>saved</item>
<item>templates</item> <item>templates</item>
@ -1396,4 +1404,18 @@
<item>jpg</item> <item>jpg</item>
</array> </array>
<string-array name="prevent_duplicate_downloads">
<item>@string/disabled</item>
<item>@string/archive</item>
<item>@string/prevent_duplicate_url_type</item>
<item>@string/prevent_duplicate_config</item>
</string-array>
<string-array name="prevent_duplicate_downloads_values">
<item></item>
<item>download_archive</item>
<item>url_type</item>
<item>config</item>
</string-array>
</resources> </resources>

View file

@ -1,2 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources></resources> <resources>
<dimen name="elevation_6dp">6dp</dimen>
<dimen name="elevation_2dp">2dp</dimen>
</resources>

View file

@ -360,8 +360,8 @@
<string name="bitrate">Bitrate</string> <string name="bitrate">Bitrate</string>
<string name="save_auto_subs">Save Automatic Subtitles</string> <string name="save_auto_subs">Save Automatic Subtitles</string>
<string name="not_deleted">Not Deleted</string> <string name="not_deleted">Not Deleted</string>
<string name="download_archive">Download Archive</string> <string name="archive">Download Archive [Use yt-dlp\'s download archive file to prevent duplicate downloads instead of the built in functionality]</string>
<string name="download_archive_summary">Use yt-dlp\'s download archive file to prevent duplicate downloads instead of the built in functionality</string> <string name="disabled">Disabled</string>
<string name="swipe_gestures_download_card">Swipe Gestures in download card</string> <string name="swipe_gestures_download_card">Swipe Gestures in download card</string>
<string name="show_download_count">Show counts in the download queue screen</string> <string name="show_download_count">Show counts in the download queue screen</string>
<string name="thumbnail_format">Thumbnail Format</string> <string name="thumbnail_format">Thumbnail Format</string>
@ -382,4 +382,13 @@
<string name="display_over_apps">Display Over Apps</string> <string name="display_over_apps">Display Over Apps</string>
<string name="display_over_apps_summary">Make the download card always display on top. Turn this on if the currently running app closes when the download card shows up</string> <string name="display_over_apps_summary">Make the download card always display on top. Turn this on if the currently running app closes when the download card shows up</string>
<string name="hour">Hour</string> <string name="hour">Hour</string>
<string name="subtitles">Subtitles</string>
<string name="prevent_duplicate_downloads">Prevent Duplicate Downloads</string>
<string name="prevent_duplicate_url_type"><![CDATA[URL & Download Type]]></string>
<string name="prevent_duplicate_config">Full Configuration</string>
<string name="copy_url">Copy URL</string>
<string name="move_top">Move to the Top</string>
<string name="move_bottom">Move to the Bottom</string>
<string name="scheduled">Scheduled</string>
<string name="clear_scheduled">Clear Scheduled</string>
</resources> </resources>

View file

@ -2,7 +2,7 @@
<shortcut <shortcut
android:shortcutId="search" android:shortcutId="search"
android:enabled="true" android:enabled="true"
android:icon="@drawable/ic_search" android:icon="@drawable/ic_search_red"
android:shortcutShortLabel="@string/search" android:shortcutShortLabel="@string/search"
android:shortcutDisabledMessage="@string/search"> android:shortcutDisabledMessage="@string/search">
<intent <intent
@ -19,7 +19,7 @@
<shortcut <shortcut
android:shortcutId="downloads" android:shortcutId="downloads"
android:enabled="true" android:enabled="true"
android:icon="@drawable/ic_downloads" android:icon="@drawable/ic_downloads_red"
android:shortcutShortLabel="@string/downloads" android:shortcutShortLabel="@string/downloads"
android:shortcutDisabledMessage="@string/downloads"> android:shortcutDisabledMessage="@string/downloads">
<intent <intent
@ -35,7 +35,7 @@
<shortcut <shortcut
android:shortcutId="queue" android:shortcutId="queue"
android:enabled="true" android:enabled="true"
android:icon="@drawable/baseline_downloading_24" android:icon="@drawable/baseline_downloading_red"
android:shortcutShortLabel="@string/download_queue" android:shortcutShortLabel="@string/download_queue"
android:shortcutDisabledMessage="@string/download_queue"> android:shortcutDisabledMessage="@string/download_queue">
<intent <intent
@ -51,7 +51,7 @@
<shortcut <shortcut
android:shortcutId="newTemplate" android:shortcutId="newTemplate"
android:enabled="true" android:enabled="true"
android:icon="@drawable/baseline_downloading_24" android:icon="@drawable/ic_terminal_red"
android:shortcutShortLabel="@string/new_template" android:shortcutShortLabel="@string/new_template"
android:shortcutDisabledMessage="@string/new_template"> android:shortcutDisabledMessage="@string/new_template">
<intent <intent

View file

@ -7,7 +7,7 @@
<data-extraction-rules> <data-extraction-rules>
<cloud-backup> <cloud-backup>
<!-- <!--
TODO: Use <include> and <exclude> to control what is backed up. Use <include> and <exclude> to control what is backed up.
The domain can be file, database, sharedpref, external or root. The domain can be file, database, sharedpref, external or root.
Examples: Examples:

View file

@ -27,14 +27,15 @@
android:key="quick_download" android:key="quick_download"
app:summary="@string/quick_download_summary" app:summary="@string/quick_download_summary"
app:title="@string/quick_download" /> app:title="@string/quick_download" />
<SwitchPreferenceCompat <ListPreference
android:widgetLayout="@layout/preferece_material_switch" android:defaultValue=""
app:defaultValue="false" android:entries="@array/prevent_duplicate_downloads"
android:entryValues="@array/prevent_duplicate_downloads_values"
android:icon="@drawable/baseline_archive_24" android:icon="@drawable/baseline_archive_24"
android:key="download_archive" app:key="prevent_duplicate_downloads"
app:summary="@string/download_archive_summary" app:useSimpleSummaryProvider="true"
app:title="@string/download_archive" /> app:title="@string/prevent_duplicate_downloads" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"

View file

@ -60,7 +60,7 @@
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false" app:defaultValue="false"
android:icon="@drawable/baseline_invert_colors_24" android:icon="@drawable/baseline_layers_24"
android:key="display_over_apps" android:key="display_over_apps"
app:summary="@string/display_over_apps_summary" app:summary="@string/display_over_apps_summary"
app:title="@string/display_over_apps" /> app:title="@string/display_over_apps" />

View file

@ -5,6 +5,7 @@
<locale android:name="az"/> <locale android:name="az"/>
<locale android:name="bn"/> <locale android:name="bn"/>
<locale android:name="bn-IN"/> <locale android:name="bn-IN"/>
<locale android:name="bg"/>
<locale android:name="bs"/> <locale android:name="bs"/>
<locale android:name="de"/> <locale android:name="de"/>
<locale android:name="el"/> <locale android:name="el"/>

View file

@ -55,7 +55,7 @@
android:defaultValue="ALL" android:defaultValue="ALL"
android:entries="@array/format_filtering" android:entries="@array/format_filtering"
android:entryValues="@array/format_filtering_values" android:entryValues="@array/format_filtering_values"
android:icon="@drawable/baseline_align_horizontal_left_24" android:icon="@drawable/baseline_filter_alt_24"
app:key="format_filter" app:key="format_filter"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:title="@string/format_filter" /> app:title="@string/format_filter" />

View file

@ -37,12 +37,12 @@ buildscript {
} }
plugins { plugins {
id 'com.android.application' version '8.1.1' apply false id 'com.android.application' version '8.3.1' apply false
id 'com.android.library' version '8.1.1' apply false id 'com.android.library' version '8.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
id "org.jetbrains.kotlin.plugin.serialization" version "1.8.10" apply false id "org.jetbrains.kotlin.plugin.serialization" version "1.8.10" apply false
id "org.jetbrains.kotlin.plugin.parcelize" version "1.8.10" apply false id "org.jetbrains.kotlin.plugin.parcelize" version "1.8.10" apply false
id 'com.android.test' version '8.1.1' apply false id 'com.android.test' version '8.3.1' apply false
id 'com.google.devtools.ksp' version '1.8.10-1.0.9' apply false id 'com.google.devtools.ksp' version '1.8.10-1.0.9' apply false
} }

View file

@ -0,0 +1,54 @@
## What's New
- Added AVI / FLV / MOV video container options
- Fixed app not showing the current download's command if it was a command type download when coming from multiple download sheet
- Added release year from description as metadata for audio downloads
- Set progress bar as interterminate when it reached 100% so people wont think it froze (its ffmpeg working, and it doesnt have a progress callback)
- Fix app crashing when trying to toggle on Show Terminal in share sheet
- Fix app crashing when going on landscape when you open the format list
- Fixed app not disabling the keep fragments toggle if you toggled the dont download as fragments
- Fixed app not fetching format list when some formats had None as a filesize
- Fixed app only showing the download type icon in the finished notification only when you expanded it. Now its a popping orange
- Showed navigation bar color when opening the download card
- Showed stylized codec name for generic audio formats
- Fixed app crashing when going on landscape when you open the multiple download card
- Fixed app crashing when you tapped the home button twice on foldable mode
- Fixed observe sources spamming and running every second
- Swapped around some icons
- Added new Language BULGARIAN
- Showed the download path in the finished notification
- Fixed calculating the next time observe sources should run
- Added a scheduled section in the download queue so that they dont stay in the same spot with queued items that are expected to run soon. You can see their ETA there for each item
## Duplicate checking
For a while the app had its own duplicate checking system when it checked the whole configuration with current history items or active downloads.
Since this system was too precise and even a slight change in options will consider it a new download and not an exact replica and most people were confused why the app allowed them to still download
So i created 3 methods of duplicate checking
- download archive -> uses the .txt file of yt-dlp to check if any url has been downloaded before or not
- url and type -> checks download history and running downloads to check if any item with the same url and type was downloaded
- full config -> the good ol method
----
- Removed paused button for each active download item. It didnt make sense. If you paused one item, the other items will continue to run anyway so what was the point. Instead i added a floating action button to pause and resume the whole queue
- Removed the cookie toggle throbbing every time you entered the page
- Slight changes to album_arist metadata parsing
- Fixed app downloading music file instead when using M4A music format
- Fixed app showing the grey deleted filter on present items
## Reordering download queue items
Now you can toggle the drag and drop mode in the queued tab to reorder your items with ease. Also you can now move multiple items to the top and to the bottom of the queue. or a single item
-------
- removed the plus icon in the piped instance dialog.
- combined the thumbnail cropper and resizer commands in audio downloads
- fixed app not removing audio on some pre-merged formats like in tiktok or instagram
- removed the -f bv[height<=...] and instead moved to -S res:... due to some problems when trying to quick download an item
- fix terminal sometimes not showing the finishing line of output or error
- added colors to app shortcut icons