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

View file

@ -20,7 +20,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads ORDER BY status")
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>>
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
@ -52,7 +52,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status = 'Processing' ORDER BY id LIMIT 1")
fun getFirstProcessingDownload() : DownloadItem
@Query("SELECT * FROM downloads WHERE status in('Active','ActivePaused','PausedReQueued')")
@Query("SELECT * FROM downloads WHERE status in('Active','ActivePaused')")
fun getActiveAndPausedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
@ -61,32 +61,32 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Active'")
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>
@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>
@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>>
@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>
@Query("SELECT format FROM downloads WHERE status in('Queued','QueuedPaused')")
fun getSelectedFormatFromQueued() : List<Format>
@Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status in ('Queued', 'PausedReQueued') ORDER BY downloadStartTime, id LIMIT 20")
fun getQueuedDownloadsThatAreNotScheduledChunked(currentTime: Long) : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status in ('Queued', 'Scheduled') ORDER BY downloadStartTime, id LIMIT 20")
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>>
@Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id")
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>
@RewriteQueriesToDropUnusedColumns
@ -98,7 +98,6 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'")
fun getPausedDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloads() : PagingSource<Int, DownloadItemSimple>
@ -106,6 +105,10 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloadsList() : List<DownloadItem>
@Query("SELECT id from downloads WHERE status='Scheduled' ORDER BY downloadStartTime, id DESC")
fun getScheduledDownloadIDs(): List<Long>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC")
fun getSavedDownloads() : PagingSource<Int, DownloadItemSimple>
@ -113,6 +116,10 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC")
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")
fun getDownloadById(id: Long) : DownloadItem
@ -165,10 +172,13 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE status='Processing'")
suspend fun deleteProcessing()
@Query("DELETE FROM downloads WHERE status='Scheduled'")
suspend fun deleteScheduled()
@Query("DELETE FROM downloads WHERE id in (:list)")
suspend fun deleteAllWithIDs(list: List<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()
@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")
suspend fun updateDownloadID(id: Long, newId: Long)
@ -251,6 +277,6 @@ interface DownloadDao {
@Query("SELECT id from downloads WHERE id > :item1 AND id < :item2 AND status in (:statuses) ORDER BY id DESC")
fun getIDsBetweenTwoItems(item1: Long, item2: Long, statuses: List<String>) : List<Long>
@Query("SELECT id from downloads WHERE id > :item1 AND id < :item2 AND status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id")
fun getQueuedIDsBetweenTwoItems(item1: Long, item2: Long) : List<Long>
@Query("SELECT id from downloads WHERE id > :item1 AND id < :item2 AND status in('Scheduled') ORDER BY downloadStartTime, id")
fun getScheduledIDsBetweenTwoItems(item1: Long, item2: Long) : List<Long>
}

View file

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

View file

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

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

View file

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

View file

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

View file

@ -116,11 +116,6 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
onItemClickListener.onOutputClick(item)
}
// 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)
@ -130,64 +125,18 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
when(DownloadRepository.Status.valueOf(item.status)){
DownloadRepository.Status.Active -> {
progressBar.isIndeterminate = true
val fromRadius: Int = dp(activity.resources, 30f)
val toRadius: Int = dp(activity.resources, 15f)
val animator = ValueAnimator.ofInt(fromRadius, toRadius)
animator.setDuration(500)
.addUpdateListener { animation ->
val value = animation.animatedValue as Int
pauseButton.cornerRadius = value
pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_pause_white)
pauseButton.contentDescription = activity.getString(R.string.pause)
pauseButton.isEnabled = true
pauseButton.tag = ActiveDownloadAction.Pause
}
animator.start()
cancelButton.visibility = View.GONE
cancelButton.isEnabled = true
}
DownloadRepository.Status.ActivePaused -> {
progressBar.isIndeterminate = false
val fromRadius: Int = dp(activity.resources, 15f)
val toRadius: Int = dp(activity.resources, 30f)
val animator = ValueAnimator.ofInt(fromRadius, toRadius)
animator.setDuration(500)
.addUpdateListener { animation ->
val value = animation.animatedValue as Int
pauseButton.cornerRadius = value
pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.exomedia_ic_play_arrow_white)
pauseButton.contentDescription = activity.getString(R.string.resume)
pauseButton.tag = ActiveDownloadAction.Resume
pauseButton.isEnabled = true
}
animator.start()
cancelButton.visibility = View.VISIBLE
}
DownloadRepository.Status.PausedReQueued -> {
progressBar.isIndeterminate = true
pauseButton.icon = ContextCompat.getDrawable(activity, R.drawable.ic_refresh)
pauseButton.contentDescription = activity.getString(R.string.please_wait)
pauseButton.tag = null
pauseButton.isEnabled = false
cancelButton.visibility = View.GONE
cancelButton.isEnabled = true
output.text = activity.getString(R.string.exo_download_paused)
}
else -> {}
}
pauseButton.setOnClickListener {
if (pauseButton.tag == ActiveDownloadAction.Pause){
onItemClickListener.onPauseClick(item.id, ActiveDownloadAction.Pause, position)
}else{
onItemClickListener.onPauseClick(item.id, ActiveDownloadAction.Resume, position)
}
}
}
interface OnItemClickListener {
fun onCancelClick(itemID: Long)
fun onPauseClick(itemID: Long, action: ActiveDownloadAction, position: Int)
fun onOutputClick(item: DownloadItem)
}

View file

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

View file

@ -10,6 +10,7 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
@ -76,6 +77,8 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
}
itemTitle.text = title
card.findViewById<TextView>(R.id.download_type).isVisible = false
// Format Note ----------------------------------
val formatNote = card.findViewById<TextView>(R.id.format_note)
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.database.models.DownloadItemSimple
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.Extensions.loadThumbnail
import com.deniscerri.ytdl.util.Extensions.popup
import com.deniscerri.ytdl.util.FileUtil
@ -79,6 +80,20 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
}
itemTitle.text = title.ifEmpty { item.url }
//DOWNLOAD TYPE -----------------------------
val type = card.findViewById<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

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

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

View file

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

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

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

View file

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

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

View file

@ -36,7 +36,7 @@ import kotlinx.coroutines.launch
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 infoUtil: InfoUtil
private lateinit var downloadViewModel: DownloadViewModel
@ -62,6 +62,10 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
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 {
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)
dialog.setContentView(view)
if (_items == null){
this.dismiss()
return
}
items = _items
formats = _formats!!
listener = _listener!!
sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!)
filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!)
filterBtn = view.findViewById(R.id.format_filter)
@ -368,9 +381,10 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
FormatSorting.codec -> {
val codecOrder = resources.getStringArray(R.array.video_codec_values).toMutableList()
codecOrder.removeFirst()
chosenFormats.groupBy { format -> codecOrder.indexOfFirst { format.vcodec.startsWith(it) } }
chosenFormats.groupBy { format -> codecOrder.indexOfFirst { format.vcodec.matches("^(${it})(.+)?$".toRegex()) } }
.flatMap {
it.value.sortedBy { l -> l.filesize }
it.value.sortedByDescending { l -> l.filesize }
}
}
FormatSorting.filesize -> chosenFormats

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

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

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

View file

@ -1,32 +1,55 @@
package com.deniscerri.ytdl.ui.downloads
import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.View.OnClickListener
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdl.ui.adapter.QueuedDownloadAdapter
import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.NotificationUtil
import com.google.android.material.button.MaterialButton
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
@ -35,15 +58,17 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener, OnClickListener {
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener {
private var fragmentView: View? = null
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
private lateinit var activeRecyclerView : RecyclerView
private lateinit var activeDownloads : ActiveDownloadAdapter
lateinit var downloadItem: DownloadItem
private lateinit var notificationUtil: NotificationUtil
private lateinit var pauseResume: MaterialButton
private lateinit var pause: ExtendedFloatingActionButton
private lateinit var resume: ExtendedFloatingActionButton
private lateinit var noResults: RelativeLayout
private lateinit var workManager: WorkManager
@ -57,6 +82,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
return fragmentView
}
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -65,75 +91,73 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
workManager = WorkManager.getInstance(requireContext())
activeDownloads =
ActiveDownloadAdapter(
this,
requireActivity()
)
activeDownloads = ActiveDownloadAdapter(this,requireActivity())
activeRecyclerView = view.findViewById(R.id.download_recyclerview)
activeRecyclerView.forceFastScrollMode()
activeRecyclerView.adapter = activeDownloads
activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
pauseResume = view.findViewById(R.id.pause_resume)
pause = view.findViewById(R.id.pause)
pause.isVisible = false
resume = view.findViewById(R.id.resume)
noResults = view.findViewById(R.id.no_results)
pauseResume.setOnClickListener {
if (pauseResume.text == requireContext().getString(R.string.pause)){
lifecycleScope.launch {
workManager.cancelAllWorkByTag("download")
pauseResume.isEnabled = false
pause.setOnClickListener {
lifecycleScope.launch {
workManager.cancelAllWorkByTag("download")
pause.isEnabled = false
// pause queued
withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}.forEach {
it.status = DownloadRepository.Status.QueuedPaused.toString()
downloadViewModel.updateDownload(it)
}
// pause active
withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}.forEach {
cancelItem(it.id.toInt())
it.status = DownloadRepository.Status.ActivePaused.toString()
downloadViewModel.updateDownload(it)
}
activeDownloads.notifyDataSetChanged()
pauseResume.isEnabled = true
// pause queued
withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}.forEach {
it.status = DownloadRepository.Status.QueuedPaused.toString()
downloadViewModel.updateDownload(it)
}
}else{
lifecycleScope.launch {
pauseResume.isEnabled = false
val active = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}
// pause active
withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}.forEach {
cancelItem(it.id.toInt())
it.status = DownloadRepository.Status.ActivePaused.toString()
downloadViewModel.updateDownload(it)
}
val toQueue = active.filter { it.status == DownloadRepository.Status.ActivePaused.toString() }.toMutableList()
activeDownloads.notifyDataSetChanged()
pause.isEnabled = true
}
}
resume.setOnClickListener {
lifecycleScope.launch {
resume.isEnabled = false
val active = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}
val toQueue = active.filter { it.status == DownloadRepository.Status.ActivePaused.toString() }.toMutableList()
runBlocking {
toQueue.forEach {
it.status = DownloadRepository.Status.Queued.toString()
downloadViewModel.updateDownload(it)
}
runBlocking {
downloadViewModel.queueDownloads(listOf())
}
val queuedItems = withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}
queuedItems.map {
it.status = DownloadRepository.Status.Queued.toString()
downloadViewModel.updateDownload(it)
}
pauseResume.isEnabled = true
}
runBlocking {
downloadViewModel.startDownloadWorker(listOf())
}
val queuedItems = withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}
queuedItems.map {
it.status = DownloadRepository.Status.Queued.toString()
downloadViewModel.updateDownload(it)
}
resume.isEnabled = true
}
}
@ -160,10 +184,23 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
}
}
lifecycleScope.launch {
downloadViewModel.activeAndActivePausedDownloadsCount.collectLatest {
noResults.isVisible = it == 0
}
}
lifecycleScope.launch {
downloadViewModel.activeDownloadsCount.collectLatest {
delay(200)
noResults.visibility = if (it == 0) View.VISIBLE else View.GONE
pause.isVisible = it > 0
}
}
lifecycleScope.launch {
downloadViewModel.pausedDownloadsCount.collectLatest {
resume.isVisible = it > 0
}
}
@ -171,19 +208,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
downloadViewModel.activeDownloads.collectLatest {
delay(100)
activeDownloads.submitList(it)
if (it.size > 1){
pauseResume.visibility = View.VISIBLE
if (it.all { l -> l.status == DownloadRepository.Status.ActivePaused.toString() }){
pauseResume.text = requireContext().getString(R.string.resume)
}else{
pauseResume.text = requireContext().getString(R.string.pause)
}
}else{
pauseResume.visibility = View.GONE
}
}
}
}
@ -210,46 +235,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
cancelDownload(itemID)
}
override fun onPauseClick(itemID: Long, action: ActiveDownloadAdapter.ActiveDownloadAction, position: Int) {
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
when(action){
ActiveDownloadAdapter.ActiveDownloadAction.Pause -> {
lifecycleScope.launch {
cancelItem(itemID.toInt())
item.status = DownloadRepository.Status.ActivePaused.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
}
}
ActiveDownloadAdapter.ActiveDownloadAction.Resume -> {
lifecycleScope.launch {
item.status = DownloadRepository.Status.PausedReQueued.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
}
withContext(Dispatchers.IO){
downloadViewModel.getQueued().filter { it.status != DownloadRepository.Status.Queued.toString() }.forEach {
it.status = DownloadRepository.Status.Queued.toString()
downloadViewModel.updateDownload(it)
}
}
}
}
}
}
}
override fun onOutputClick(item: DownloadItem) {
if (item.logID != null && item.logID != 0L) {
val bundle = Bundle()
@ -279,7 +264,4 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
}
}
}
override fun onClick(p0: View?) {
}
}

View file

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

View file

@ -1,22 +1,26 @@
package com.deniscerri.ytdl.ui.downloads
import android.animation.AnimatorSet
import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -31,7 +35,7 @@ import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdl.ui.adapter.QueuedDownloadAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdl.util.Extensions.toListString
@ -39,6 +43,7 @@ import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.card.MaterialCardView
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
@ -51,15 +56,16 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener {
class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickListener {
private var fragmentView: View? = null
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
private lateinit var queuedRecyclerView : RecyclerView
private lateinit var adapter : GenericDownloadAdapter
private lateinit var adapter : QueuedDownloadAdapter
private lateinit var noResults : RelativeLayout
private lateinit var notificationUtil: NotificationUtil
private lateinit var fileSize: TextView
private lateinit var dragHandleToggle: TextView
private var totalSize: Int = 0
private var actionMode : ActionMode? = null
@ -75,21 +81,20 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
return fragmentView
}
@SuppressLint("SetTextI18n")
@SuppressLint("SetTextI18n", "RestrictedApi")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fileSize = view.findViewById(R.id.filesize)
adapter =
GenericDownloadAdapter(
this,
requireActivity()
)
dragHandleToggle = view.findViewById(R.id.drag)
val itemTouchHelper = ItemTouchHelper(queuedDragDropHelper)
adapter = QueuedDownloadAdapter(this, requireActivity(), itemTouchHelper)
noResults = view.findViewById(R.id.no_results)
queuedRecyclerView = view.findViewById(R.id.download_recyclerview)
queuedRecyclerView.forceFastScrollMode()
queuedRecyclerView.adapter = adapter
queuedRecyclerView.enableFastScroll()
itemTouchHelper.attachToRecyclerView(queuedRecyclerView)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("queued")){
@ -126,83 +131,12 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused)).observe(viewLifecycleOwner){
totalSize = it
noResults.visibility = if (it == 0) View.VISIBLE else View.GONE
noResults.isVisible = it == 0
dragHandleToggle.isVisible = it > 1
}
}
override fun onActionButtonClick(itemID: Long) {
removeItem(itemID)
}
override fun onCardClick(itemID: Long) {
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
UiUtil.showDownloadItemDetailsCard(
item,
requireActivity(),
DownloadRepository.Status.valueOf(item.status),
removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
sheet.hide()
removeItem(it.id)
},
downloadItem = {
downloadViewModel.deleteDownload(it.id)
it.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(it))
}
},
longClickDownloadButton = {
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
Pair("downloadItem", it),
Pair("result", downloadViewModel.createResultItemFromDownload(it)),
Pair("type", it.type)
))
},
scheduleButtonClick = {downloadItem ->
UiUtil.showDatePicker(parentFragmentManager) {
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(downloadItem.id)
downloadItem.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(downloadItem))
}
}
}
)
}
}
override fun onCardSelect(isChecked: Boolean, position: Int) {
lifecycleScope.launch {
val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
val now = System.currentTimeMillis()
actionMode?.apply {
if (selectedObjects == 0){
this.finish()
}else{
this.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.download).isVisible = withContext(Dispatchers.IO){
downloadViewModel.checkAllQueuedItemsAreScheduledAfterNow(adapter.checkedItems.toList(), adapter.inverted, now)
}
this.menu.findItem(R.id.up).isVisible = position > 0
this.menu.findItem(R.id.select_between).isVisible = false
if(selectedObjects == 2){
val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused).toListString())
}
this.menu.findItem(R.id.select_between).isVisible = idsInMiddle.isNotEmpty()
}
}
}
dragHandleToggle.setOnClickListener {
adapter.toggleShowDragHandle()
}
}
@ -255,7 +189,8 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
lifecycleScope.launch {
val selectedIDs = getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getQueuedIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last())
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused).toListString())
}.toMutableList()
idsInMiddle.addAll(selectedIDs)
if (idsInMiddle.isNotEmpty()){
@ -324,6 +259,17 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
}
true
}
R.id.down -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
adapter.clearCheckedItems()
withContext(Dispatchers.IO){
downloadViewModel.putAtBottomOfQueue(selectedObjects)
}
actionMode?.finish()
}
true
}
R.id.copy_urls -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
@ -420,4 +366,186 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
)
}
}
var movedToNewPositionID = 0L
private val queuedDragDropHelper: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPosition = viewHolder.bindingAdapterPosition
val toPosition = target.bindingAdapterPosition
movedToNewPositionID = target.itemView.tag.toString().toLong()
adapter.notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
override fun onSelectedChanged(
viewHolder: RecyclerView.ViewHolder?,
actionState: Int
) {
super.onSelectedChanged(viewHolder, actionState)
if (ItemTouchHelper.ACTION_STATE_DRAG == actionState) {
/**
* Change alpha, scale and elevation on drag.
*/
(viewHolder?.itemView as? MaterialCardView)?.also {
AnimatorSet().apply {
this.duration = 100L
this.interpolator = AccelerateDecelerateInterpolator()
playTogether(
UiUtil.getAlphaAnimator(it, 0.7f),
UiUtil.getScaleXAnimator(it, 1.02f),
UiUtil.getScaleYAnimator(it, 1.02f),
UiUtil.getElevationAnimator(it, R.dimen.elevation_6dp)
)
}.start()
}
}
}
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
super.clearView(recyclerView, viewHolder)
/**
* Clear alpha, scale and elevation after drag/swipe
*/
(viewHolder.itemView as? MaterialCardView)?.also {
AnimatorSet().apply {
this.duration = 100L
this.interpolator = AccelerateDecelerateInterpolator()
playTogether(
UiUtil.getAlphaAnimator(it, 1f),
UiUtil.getScaleXAnimator(it, 1f),
UiUtil.getScaleYAnimator(it, 1f),
UiUtil.getElevationAnimator(it, R.dimen.elevation_2dp)
)
}.start()
}
downloadViewModel.putAtPosition(viewHolder.itemView.tag.toString().toLong(), movedToNewPositionID)
}
override fun isLongPressDragEnabled(): Boolean {
return false
}
}
override fun onMoveQueuedItemToTop(itemID: Long) {
lifecycleScope.launch {
downloadViewModel.putAtTopOfQueue(listOf(itemID))
}
}
override fun onMoveQueuedItemToBottom(itemID: Long) {
lifecycleScope.launch {
downloadViewModel.putAtBottomOfQueue(listOf(itemID))
}
}
override fun onQueuedCardClick(itemID: Long) {
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
UiUtil.showDownloadItemDetailsCard(
item,
requireActivity(),
DownloadRepository.Status.valueOf(item.status),
removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
sheet.hide()
removeItem(it.id)
},
downloadItem = {
downloadViewModel.deleteDownload(it.id)
it.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(it))
}
},
longClickDownloadButton = {
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
Pair("downloadItem", it),
Pair("result", downloadViewModel.createResultItemFromDownload(it)),
Pair("type", it.type)
)
)
},
scheduleButtonClick = {downloadItem ->
UiUtil.showDatePicker(parentFragmentManager) {
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(downloadItem.id)
downloadItem.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(downloadItem))
}
}
}
)
}
}
override fun onQueuedCardSelect(isChecked: Boolean, position: Int) {
lifecycleScope.launch {
val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
val now = System.currentTimeMillis()
actionMode?.apply {
if (selectedObjects == 0){
this.finish()
}else{
this.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.download).isVisible = withContext(Dispatchers.IO){
downloadViewModel.checkAllQueuedItemsAreScheduledAfterNow(adapter.checkedItems.toList(), adapter.inverted, now)
}
this.menu.findItem(R.id.up).isVisible = position > 0
this.menu.findItem(R.id.down).isVisible = position < totalSize
this.menu.findItem(R.id.select_between).isVisible = false
if(selectedObjects == 2){
val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused).toListString())
}
this.menu.findItem(R.id.select_between).isVisible = idsInMiddle.isNotEmpty()
}
}
}
}
}
override fun onQueuedCancelClick(itemID: Long) {
cancelDownload(itemID)
}
private fun cancelDownload(itemID: Long){
lifecycleScope.launch {
cancelItem(itemID.toInt())
withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}.let {
it.status = DownloadRepository.Status.Cancelled.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(it)
}
}
}
}
private fun cancelItem(id: Int){
YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id)
}
}

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)
useCookies.isChecked = useCookiesPref
useCookies.jumpDrawablesToCurrentState()
newCookie.isEnabled = useCookiesPref
}

View file

@ -170,6 +170,7 @@ class DownloadLogFragment : Fragment() {
if (logItem != null){
if (logItem.content.isNotBlank()) {
content.setText(logItem.content, TextView.BufferType.SPANNABLE)
bottomAppBar?.menu?.get(1)?.isVisible = contentScrollView.canScrollVertically(1)
}
if (!bottomAppBar.menu.children.first { it.itemId == R.id.scroll_down }.isVisible){
content.scrollTo(0, content.height)

View file

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

View file

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

View file

@ -115,7 +115,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
showTerminalShareIcon!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { pref: Preference?, _: Any ->
val packageManager = requireContext().packageManager
val aliasComponentName = ComponentName(requireContext(), "com.deniscerri.ytdlnis.terminalShareAlias")
val aliasComponentName = ComponentName(requireContext(), "com.deniscerri.ytdl.terminalShareAlias")
if ((pref as SwitchPreferenceCompat).isChecked){
packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,

View file

@ -1,6 +1,7 @@
package com.deniscerri.ytdl.util
import android.content.Context
import android.util.Log
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.LogItem
@ -15,6 +16,7 @@ class CrashListener(private val context: Context) : Thread.UncaughtExceptionHand
override fun uncaughtException(p0: Thread, p1: Throwable) {
p1.message?.apply {
Log.e("ERROR", this)
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
createLog(this@apply)
}

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdl.util
import android.R.color
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Dialog
@ -9,7 +10,9 @@ import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Outline
import android.graphics.drawable.Drawable
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.media.MediaMetadataRetriever
@ -27,6 +30,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.Px
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStarted
@ -335,10 +339,13 @@ object Extensions {
return cookie.toString()
}
fun ObserveSourcesItem.calculateNextTime() : Long {
fun ObserveSourcesItem.calculateNextTimeForObserving() : Long {
val item = this
val now = System.currentTimeMillis()
Calendar.getInstance().apply {
if (item.everyCategory != com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory.HOUR){
timeInMillis = item.startsTime
if (item.everyCategory != EveryCategory.HOUR){
val hourMin = Calendar.getInstance()
hourMin.timeInMillis = item.everyTime
@ -346,38 +353,40 @@ object Extensions {
set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE))
}
when(item.everyCategory){
EveryCategory.HOUR -> {
add(Calendar.HOUR, item.everyNr)
}
EveryCategory.DAY -> {
add(Calendar.DAY_OF_MONTH, item.everyNr)
}
EveryCategory.WEEK -> {
item.weeklyConfig?.apply {
if (this.weekDays.isEmpty()){
add(Calendar.DAY_OF_MONTH, 7 * item.everyNr)
}else{
var weekDayNr = get(Calendar.DAY_OF_WEEK) - 1
if (weekDayNr == 0) weekDayNr = 7
val followingWeekDay = this.weekDays.firstOrNull { it > weekDayNr }
if (followingWeekDay == null){
add(Calendar.DAY_OF_MONTH, this.weekDays.minBy { it } + (7 - weekDayNr))
item.everyNr--
}else{
add(Calendar.DAY_OF_MONTH, followingWeekDay.toInt() - weekDayNr)
}
if (item.everyNr > 1){
while (timeInMillis < now){
when(item.everyCategory){
EveryCategory.HOUR -> {
add(Calendar.HOUR, item.everyNr)
}
EveryCategory.DAY -> {
add(Calendar.DAY_OF_MONTH, item.everyNr)
}
EveryCategory.WEEK -> {
item.weeklyConfig?.apply {
if (this.weekDays.isEmpty()){
add(Calendar.DAY_OF_MONTH, 7 * item.everyNr)
}else{
var weekDayNr = get(Calendar.DAY_OF_WEEK) - 1
if (weekDayNr == 0) weekDayNr = 7
val followingWeekDay = this.weekDays.firstOrNull { it > weekDayNr }
if (followingWeekDay == null){
add(Calendar.DAY_OF_MONTH, this.weekDays.minBy { it } + (7 - weekDayNr))
item.everyNr--
}else{
add(Calendar.DAY_OF_MONTH, followingWeekDay.toInt() - weekDayNr)
}
if (item.everyNr > 1){
add(Calendar.DAY_OF_MONTH, 7 * item.everyNr)
}
}
}
}
}
EveryCategory.MONTH -> {
add(Calendar.MONTH, item.everyNr)
item.monthlyConfig?.apply {
set(Calendar.DAY_OF_MONTH, this.everyMonthDay)
EveryCategory.MONTH -> {
add(Calendar.MONTH, item.everyNr)
item.monthlyConfig?.apply {
set(Calendar.DAY_OF_MONTH, this.everyMonthDay)
}
}
}
}
@ -392,7 +401,14 @@ object Extensions {
drawable!!.intrinsicWidth,
drawable.intrinsicHeight, Bitmap.Config.ARGB_8888
)
val paint = Paint()
val colorValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.colorActivatedHighlight, colorValue, true)
paint.setColorFilter(PorterDuffColorFilter(colorValue.data, PorterDuff.Mode.SRC_IN))
val canvas = Canvas(bitmap)
DrawableCompat.setTint(drawable, colorValue.data)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap

View file

@ -763,6 +763,14 @@ class InfoUtil(private val context: Context) {
format.put("filesize", size)
}catch (ignored: Exception){}
}
if (format.has("filesize_approx")){
if (format.get("filesize_approx") == "None"){
format.remove("filesize_approx")
format.put("filesize_approx", 0)
}
}
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
if (formatProper.format_note == null) continue
@ -1089,7 +1097,7 @@ class InfoUtil(private val context: Context) {
}
}
if (sharedPreferences.getBoolean("download_archive", false)){
if (sharedPreferences.getString("prevent_duplicate_downloads", "")!! == "download_archive"){
request.addOption("--download-archive", FileUtil.getDownloadArchivePath(context))
}
@ -1152,7 +1160,7 @@ class InfoUtil(private val context: Context) {
if (embedMetadata){
request.addOption("--embed-metadata")
request.addOption("--parse-metadata", "artist:(?P<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", "%(dscrptn_year,release_year,release_date>%Y,upload_date>%Y)s:%(meta_date)s")
@ -1166,21 +1174,29 @@ class InfoUtil(private val context: Context) {
}
val cropThumb = downloadItem.audioPreferences.cropThumb ?: sharedPreferences.getBoolean("crop_thumbnail", true)
if (thumbnailFormat == "jpg"){
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
}
if (downloadItem.audioPreferences.embedThumb){
request.addOption("--embed-thumbnail")
if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!)
val thumbnailConfig = StringBuilder("")
val cropConfig = """-vf crop=\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\"""""
if (thumbnailFormat == "jpg") thumbnailConfig.append("""--ppa "ThumbnailsConvertor:-qmin 1 -q:v 1"""")
if (cropThumb){
if (thumbnailFormat == "jpg") {
thumbnailConfig.deleteCharAt(thumbnailConfig.length - 1)
thumbnailConfig.append(""" $cropConfig""")
}
else thumbnailConfig.append("""--ppa "ThumbnailsConvertor:$cropConfig""")
}
if (thumbnailConfig.isNotBlank()){
runCatching {
val config = File(context.cacheDir.absolutePath + "/config" + downloadItem.id + "##ffmpegCrop.txt")
val configData = "--ppa \"ThumbnailsConvertor:-vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
config.writeText(configData)
config.writeText(thumbnailConfig.toString())
request.addOption("--config", config.absolutePath)
}
}
}
if (filenameTemplate.isNotBlank()){
@ -1220,19 +1236,20 @@ class InfoUtil(private val context: Context) {
//format logic
var videoF = downloadItem.format.format_id
var audioF = downloadItem.videoPreferences.audioFormatIDs.joinToString("+") { f ->
var altAudioF = ""
var audioF = downloadItem.videoPreferences.audioFormatIDs.map { f ->
val format = downloadItem.allFormats.find { it.format_id == f }
format?.run {
if (this.format_id.contains("-")) {
if (!this.lang.isNullOrBlank() && this.lang != "None") {
"ba[format_id~='^(${this.format_id.split("-")[0]})'][language^=${this.lang}]"
} else {
"${this.format_id}/${this.format_id.split("-")[0]}"
altAudioF = this.format_id.split("-")[0]
this.format_id
}
} else this.format_id
} ?: f
}.ifBlank { "ba" }
}.joinToString("+").ifBlank { "ba" }
val preferredAudioLanguage = sharedPreferences.getString("audio_language", "")!!
if (downloadItem.videoPreferences.removeAudio) audioF = ""
@ -1244,13 +1261,18 @@ class InfoUtil(private val context: Context) {
val defaultFormats = context.resources.getStringArray(R.array.video_formats_values)
val usingGenericFormat = defaultFormats.contains(videoF) || downloadItem.allFormats.isEmpty() || downloadItem.allFormats == getGenericVideoFormats(context.resources)
var hasGenericResulutionFormat = ""
if (!usingGenericFormat){
// with audio removed
if (audioF.isBlank()){
f.append("$videoF/bv/b")
}else{
//with audio
f.append("$videoF+$audioF/$videoF+ba/$videoF/b")
f.append("$videoF+$audioF/")
if (altAudioF.isNotBlank()){
f.append("$videoF+$altAudioF/")
}
f.append("$videoF+ba/$videoF/b")
if (audioF.count("+") > 0){
request.addOption("--audio-multistreams")
@ -1263,7 +1285,8 @@ class InfoUtil(private val context: Context) {
videoF = "wv"
if (audioF == "ba") audioF = "wa"
}else if (defaultFormats.contains(videoF)) {
videoF = "bv[height<="+videoF.split("_")[0].dropLast(1)+"]"
hasGenericResulutionFormat = videoF.split("_")[0].dropLast(1)
videoF = "bv"
}
val preferredFormatIDs = sharedPreferences.getString("format_id", "").toString()
@ -1331,6 +1354,7 @@ class InfoUtil(private val context: Context) {
}
StringBuilder().apply {
if (hasGenericResulutionFormat.isNotBlank()) append(",res:${hasGenericResulutionFormat}")
if (sharedPreferences.getBoolean("prefer_smaller_formats", false)) append(",+size")
if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref")
if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref")
@ -1369,7 +1393,9 @@ class InfoUtil(private val context: Context) {
if (downloadItem.videoPreferences.removeAudio){
request.addOption("--ppa", "ffmpeg:-an")
request.addOption("--use-postprocessor", "FFmpegCopyStream")
request.addOption("--ppa", "CopyStream:-c copy -an")
}
request.addOption("-P", downDir.absolutePath)
@ -1458,7 +1484,15 @@ class InfoUtil(private val context: Context) {
val audioFormatsValues = resources.getStringArray(R.array.audio_formats_values)
val formats = mutableListOf<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.reverse()
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.Intent
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
@ -243,19 +244,18 @@ class NotificationUtil(var context: Context) {
}
val bitmap = iconType.toBitmap(context)
val bigPictureStyle = NotificationCompat.BigPictureStyle()
bigPictureStyle.bigLargeIcon(bitmap)
if (Build.VERSION.SDK_INT >= 31){
bigPictureStyle.showBigPictureWhenCollapsed(true)
}
notificationBuilder
.setContentTitle("${res.getString(R.string.downloaded)} $title")
.setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon(bitmap)
.setGroup(DOWNLOAD_FINISHED_NOTIFICATION_ID.toString())
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.setContentText(title)
.setStyle(bigPictureStyle)
.setStyle(NotificationCompat.BigTextStyle()
.bigText("""
$title
${filepath?.joinToString("\n")}
""".trimIndent()))
.setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions()
@ -404,7 +404,7 @@ class NotificationUtil(var context: Context) {
)
try {
notificationBuilder.setProgress(100, progress, progress == 0)
notificationBuilder.setProgress(100, progress, (progress == 0 || progress == 100))
.setContentTitle(title)
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
.clearActions()

View file

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

View file

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

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

View file

@ -25,8 +25,10 @@ import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
@ -140,29 +142,23 @@ class TerminalDownloadWorker(
}
}
return runBlocking {
if (logDownloads) logRepo.update(it.out, logItem.id)
dao.updateLog(it.out, itemId.toLong())
dao.delete(itemId.toLong())
notificationUtil.cancelDownloadNotification(itemId)
Result.success()
}
if (logDownloads) logRepo.update(it.out, logItem.id)
dao.updateLog(it.out, itemId.toLong())
notificationUtil.cancelDownloadNotification(itemId)
delay(1000)
dao.delete(itemId.toLong())
Result.success()
}.onFailure {
return runBlocking {
CoroutineScope(Dispatchers.IO).launch {
if (it.message != null){
if (logDownloads) logRepo.update(it.message!!, logItem.id)
dao.updateLog(it.message!!, itemId.toLong())
dao.delete(itemId.toLong())
}
}
notificationUtil.cancelDownloadNotification(itemId)
File(FileUtil.getDefaultCommandPath() + "/" + itemId).deleteRecursively()
Log.e(TAG, context.getString(R.string.failed_download), it)
Result.failure()
if (it.message != null){
if (logDownloads) logRepo.update(it.message!!, logItem.id)
dao.updateLog(it.message!!, itemId.toLong())
}
notificationUtil.cancelDownloadNotification(itemId)
File(FileUtil.getDefaultCommandPath() + "/" + itemId).deleteRecursively()
Log.e(TAG, context.getString(R.string.failed_download), it)
delay(1000)
dao.delete(itemId.toLong())
Result.failure()
}

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"
>
<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
android:id="@+id/active_download_delete"
style="?attr/materialIconButtonFilledStyle"
@ -191,7 +174,7 @@
android:layout_height="wrap_content"
app:backgroundTint="?attr/colorSurface"
app:cornerRadius="15dp"
app:icon="@drawable/baseline_delete_24"
app:icon="@drawable/baseline_close_24"
app:iconSize="30dp"
app:iconTint="?android:textColorPrimary"
app:layout_constraintBottom_toTopOf="@+id/output"

View file

@ -1,7 +1,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_height="wrap_content"
android:backgroundTint="@android:color/transparent"
android:checkable="true"
android:clickable="true"
android:focusable="true"
@ -12,66 +13,53 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:cardCornerRadius="10dp"
android:layout_marginVertical="10dp"
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: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
android:layout_width="match_parent"
android:paddingHorizontal="10dp"
android:paddingVertical="10dp"
android:layout_height="wrap_content">
android:layout_height="80dp">
<FrameLayout
android:id="@+id/image_frame"
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/downloads_image_view"
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_constraintTop_toTopOf="parent"
app:layout_constraintDimensionRatio="H,16:9"
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>
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/download_item_data"
app:layout_constraintVertical_bias="0.0"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/active_download_pause"
app:layout_constraintHorizontal_weight="0.7"
app:layout_constraintStart_toEndOf="@+id/image_frame"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintEnd_toStartOf="@+id/options"
app:layout_constraintStart_toEndOf="@+id/downloads_image_view"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<TextView
android:id="@+id/title"
@ -91,8 +79,6 @@
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"
@ -100,74 +86,118 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title">
<TextView
android:id="@+id/format_note"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
<LinearLayout
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" />
android:layout_height="wrap_content">
<TextView
android:id="@+id/download_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorPrimaryContainer"
android:clickable="false"
android:gravity="center"
android:paddingHorizontal="5dp"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
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>
</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
android:id="@+id/active_download_delete"
style="@style/Widget.Material3.Button.IconButton"
app:layout_constraintVertical_bias="0.0"
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/options"
android:layout_width="wrap_content"
android:contentDescription="@string/Remove"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
app:cornerRadius="15dp"
app:icon="@drawable/baseline_delete_24"
app:iconSize="30dp"
app:iconTint="?android:textColorPrimary"
android:layout_gravity="end|center_vertical"
android:clickable="false"
android:focusable="false"
android:padding="16dp"
android:tintMode="src_in"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="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="download_item_data,active_download_delete" />
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/baseline_more_vert_24"
app:tint="?attr/colorControlNormal"
tools:ignore="ContentDescription" />
</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_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"
@ -150,24 +188,7 @@
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" />
</LinearLayout>

View file

@ -9,18 +9,6 @@
android:layout_height="match_parent"
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
android:id="@+id/download_recyclerview"
android:layout_width="match_parent"
@ -30,12 +18,39 @@
android:scrollbars="none"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
android:paddingBottom="100dp"
app:layout_constraintTop_toBottomOf="@+id/pause_resume">
</androidx.recyclerview.widget.RecyclerView>
</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"
android:visibility="gone"/>
android:visibility="gone"
android:layout_width="match_parent"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -11,19 +11,36 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/filesize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="30dp"
android:paddingVertical="10dp"
android:visibility="gone"
android:text="@string/file_size"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/filesize"
android:layout_width="0dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:paddingHorizontal="30dp"
android:paddingVertical="10dp"
android:text="@string/file_size"
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
android:id="@+id/download_recyclerview"

View file

@ -76,9 +76,9 @@
android:paddingEnd="10dp"
android:scrollbars="none"
android:shadowColor="@color/black"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="1.5"
android:shadowDx="2"
android:shadowDy="2"
android:shadowRadius="1"
android:textColor="#FFF"
android:textSize="11sp"
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
android:layout_width="match_parent"
android:paddingHorizontal="15dp"
android:paddingBottom="15dp"
android:scrollbars="none"
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"
app:showAsAction="never" />
<item
android:id="@+id/clear_scheduled"
android:icon="@drawable/ic_delete_all"
android:title="@string/clear_scheduled"
app:showAsAction="never" />
<item
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:icon="@drawable/ic_arrow_up"
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
android:id="@+id/download"

View file

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

View file

@ -351,14 +351,13 @@
<string name="bitrate">Bit Sürəti</string>
<string name="after">Sonra</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="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="starts">Başlayır</string>
<string name="occurrences">proses</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="add">Əlavə et</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="save_auto_subs">Uložit automatické titulky</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="new_source">Nový zfroj</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="month">Měsíc</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="thumbnail_format">Formát miniatur</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="save_auto_subs">Guardar subtítulos automáticos</string>
<string name="not_deleted">No eliminado</string>
<string name="download_archive">Descargar archivo</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="archive">Descargar archivo [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="week">Semana</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="save_auto_subs">स्वचालित उपशीर्षक सहेजें</string>
<string name="not_deleted">हटाया नहीं गया</string>
<string name="download_archive">डाउनलोड संग्रह</string>
<string name="archive">डाउनलोड संग्रह [डुप्लिकेट डाउनलोड को रोकने के लिए अंतर्निहित कार्यक्षमता के बजाय yt-dlp की डाउनलोड संग्रह फ़ाइल का उपयोग करें]</string>
<string name="on_time">नियत समय पर</string>
<string name="delete_all_sources">सभी स्रोत हटाएँ</string>
<string name="force_ipv4">IPv4 को बाध्य करें</string>
<string name="never">कभी नहीं</string>
<string name="copy_urls">URL कॉपी करें</string>
<string name="occurrences">घटनाएँ</string>
<string name="download_archive_summary">डुप्लिकेट डाउनलोड को रोकने के लिए अंतर्निहित कार्यक्षमता के बजाय yt-dlp की डाउनलोड संग्रह फ़ाइल का उपयोग करें</string>
<string name="retry_missing_downloads">गुम हुए डाउनलोड पुनः प्रयास करें</string>
<string name="thumbnail_format">थंबनेल प्रारूप</string>
<string name="get_new_uploads">केवल नए अपलोड प्राप्त करें</string>

View file

@ -352,8 +352,7 @@
<string name="bitrate">Bitráta</string>
<string name="save_auto_subs">Feliratok automatikus mentése</string>
<string name="not_deleted">Nincs törölve</string>
<string name="download_archive">Archívum letöltése</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="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="on_time">Be</string>
<string name="occurrences">események</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="bitrate">Laju Bit</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="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="not_deleted">Tidak Dihapus</string>
<string name="changelog">Log Perubahan</string>

View file

@ -351,7 +351,7 @@
<string name="bitrate">Velocità di trasmissione</string>
<string name="save_auto_subs">Salva i Sottotitoli Automatici</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="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>
@ -366,7 +366,6 @@
<string name="confirm_delete_sources_desc">Cancella tutti i sorgenti, permanentemente</string>
<string name="copy_urls">Copia URLs</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="get_new_uploads">Ricevi Solo Nuovi Upload</string>
<string name="also_download_audio">Scarica Anche come Audio</string>

View file

@ -345,9 +345,8 @@
<string name="week"></string>
<string name="retry_missing_downloads">ダウンロード失敗時に再試行</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="download_archive_summary">内蔵の機能ではなく yt-dlp のダウンロード記録ファイルを使い重複ダウンロードを防止</string>
<string name="force_ipv4_desc">すべての接続を IPv4 で行う</string>
<string name="observe_sources">指定先を監視</string>
<string name="new_source">新しい監視先</string>

View file

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

View file

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

View file

@ -354,8 +354,7 @@
<string name="set_time">Ustaw czas</string>
<string name="starts">Uruchom</string>
<string name="retry_missing_downloads">Spróbuj pobrać ponownie</string>
<string name="download_archive">Pobierz archiwum</string>
<string name="download_archive_summary">Użyj pliku archiwum pobierania yt-dlp, aby zapobiec duplikatom pobierania zamiast wbudowanej funkcjonalności</string>
<string name="archive">Pobierz archiwum [Użyj pliku archiwum pobierania yt-dlp, aby zapobiec duplikatom pobierania zamiast wbudowanej funkcjonalności]</string>
<string name="month">Miesiąc</string>
<string name="not_deleted">Nie usunięte</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="bitrate">Taxa de bits</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="delete_all_sources">Excluir todas as origens</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="day">Dia(s)</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="force_ipv4_desc">Efetuar todas as conexões com IPv4</string>
<string name="thumbnail_format">Formato da miniatura</string>

View file

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

View file

@ -346,7 +346,6 @@
<string name="on_time">Вкл</string>
<string name="day">День</string>
<string name="occurrences">случаев</string>
<string name="download_archive_summary">Используйте архивный файл загрузок yt-dlp, чтобы предотвратить дублирование загрузок вместо встроенной функции</string>
<string name="observe_sources">Источники загрузки</string>
<string name="new_source">Новый источник</string>
<string name="confirm_delete_sources_desc">Удалить все источники навсегда</string>
@ -358,7 +357,7 @@
<string name="bitrate">Битрейт</string>
<string name="save_auto_subs">Сохранить автоматически субтитры</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="show_download_count">Показывать кол-во контента на экране очереди загрузки</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="save_auto_subs">Uložiť automatické titulky</string>
<string name="not_deleted">Nevymazané</string>
<string name="download_archive">Stiahnuť archív</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="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="copy_urls">Kopírovať adresy URL</string>
<string name="observe_sources">Sledovať zdroje</string>
<string name="new_source">Nový zdroj</string>

View file

@ -357,8 +357,7 @@
<string name="ends">Mbaron</string>
<string name="day">Ditë</string>
<string name="month">Muaj</string>
<string name="download_archive">Arkiva e Shkarkimeve</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="archive">Arkiva e Shkarkimeve [Përdor skedarin e arkivës të yt-dlp për të parandaluar shkarkimet duplicate]</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="also_download_audio">Shkarko Gjithashtu si Audio</string>

View file

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

View file

@ -356,12 +356,11 @@
<string name="bitrate">Bit hızı</string>
<string name="save_auto_subs">Otomatik Altyazıları Kaydet</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="never">Asla</string>
<string name="on_time">Çalışmakta</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="get_new_uploads">Yalnızca Yeni Yüklemeleri Alın</string>
<string name="also_download_audio">Ayrıca Ses Olarak İndir</string>

View file

@ -357,8 +357,7 @@
<string name="month">Місяць</string>
<string name="not_deleted">Не видалено</string>
<string name="save_auto_subs">Зберегти автоматично субтитри</string>
<string name="download_archive">Архів завантажень</string>
<string name="download_archive_summary">Використовуйте файл архіву завантажень yt-dlp, щоб запобігти повторним завантаженням замість вбудованої функції</string>
<string name="archive">Архів завантажень [Використовуйте файл архіву завантажень yt-dlp, щоб запобігти повторним завантаженням замість вбудованої функції]</string>
<string name="swipe_gestures_download_card">Жести пальцем у картці завантаження</string>
<string name="show_download_count">Показати підрахунки на екрані черги завантаження</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="copy_urls">Sao chép URL</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="download_archive">Tải xuống lưu trữ</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="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="all">Tất cả</string>

View file

@ -352,13 +352,12 @@
<string name="ends">结束</string>
<string name="after"></string>
<string name="day"></string>
<string name="download_archive_summary">使用 yt-dlp 的下载存档文件而非应用的内置功能来防止重复下载</string>
<string name="month"></string>
<string name="retry_missing_downloads">重试缺少的下载</string>
<string name="not_deleted">未删除</string>
<string name="bitrate">比特率</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="wrap_text">换行显示过长文本</string>
<string name="text_size">文本尺寸</string>

View file

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

View file

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

View file

@ -1,2 +1,5 @@
<?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="save_auto_subs">Save Automatic Subtitles</string>
<string name="not_deleted">Not Deleted</string>
<string name="download_archive">Download Archive</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="archive">Download Archive [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="show_download_count">Show counts in the download queue screen</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_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="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>

View file

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

View file

@ -7,7 +7,7 @@
<data-extraction-rules>
<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.
Examples:

View file

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

View file

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

View file

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

View file

@ -55,7 +55,7 @@
android:defaultValue="ALL"
android:entries="@array/format_filtering"
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:useSimpleSummaryProvider="true"
app:title="@string/format_filter" />

View file

@ -37,12 +37,12 @@ buildscript {
}
plugins {
id 'com.android.application' version '8.1.1' apply false
id 'com.android.library' version '8.1.1' apply false
id 'com.android.application' version '8.3.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.plugin.serialization" 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
}

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