1.7.6.1-beta

This commit is contained in:
zaednasr 2024-05-26 20:53:01 +02:00
parent 732ffa7367
commit 33de21bce9
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
49 changed files with 1537 additions and 1183 deletions

View file

@ -11,7 +11,7 @@ def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 7 def versionMinor = 7
def versionPatch = 6 def versionPatch = 6
def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionBuild = 1 // bump for dogfood builds, public betas, etc.
def isBeta = true def isBeta = true
def versionExt = isBeta ? ".${versionBuild}-beta" : "" def versionExt = isBeta ? ".${versionBuild}-beta" : ""
@ -195,4 +195,5 @@ dependencies {
implementation 'com.google.accompanist:accompanist-webview:0.30.1' implementation 'com.google.accompanist:accompanist-webview:0.30.1'
implementation 'androidx.compose.material3:material3-android:1.2.1' implementation 'androidx.compose.material3:material3-android:1.2.1'
implementation "io.noties.markwon:core:4.6.2" implementation "io.noties.markwon:core:4.6.2"
implementation("org.greenrobot:eventbus:3.3.1")
} }

View file

@ -21,7 +21,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads ORDER BY status") @Query("SELECT * FROM downloads ORDER BY status")
fun getAllDownloads() : PagingSource<Int, DownloadItem> fun getAllDownloads() : PagingSource<Int, DownloadItem>
@Query("SELECT * FROM downloads WHERE status in ('Active', 'ActivePaused')") @Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloads() : Flow<List<DownloadItem>> fun getActiveDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status = 'Processing'") @Query("SELECT * FROM downloads WHERE status = 'Processing'")
@ -38,12 +38,9 @@ interface DownloadDao {
@Query(""" @Query("""
SELECT COUNT(*) FROM downloads WHERE status = 'Processing' SELECT DISTINCT type from downloads where status = 'Processing' and id in (:ids)
UNION
SELECT COUNT(*) FROM downloads WHERE status ='Processing' AND type =
(SELECT type from downloads WHERE status = 'Processing' ORDER BY id LIMIT 1)
""") """)
fun getProcessingDownloadsCountByType() : List<Int> fun getProcessingDownloadTypes(ids: List<Long>) : List<String>
@Query("UPDATE downloads set status = 'Processing' WHERE id in (:ids)") @Query("UPDATE downloads set status = 'Processing' WHERE id in (:ids)")
@ -53,7 +50,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status = 'Processing' ORDER BY id LIMIT 1") @Query("SELECT * FROM downloads WHERE status = 'Processing' ORDER BY id LIMIT 1")
fun getFirstProcessingDownload() : DownloadItem fun getFirstProcessingDownload() : DownloadItem
@Query("SELECT * FROM downloads WHERE status in('Active','ActivePaused')") @Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveAndPausedDownloadsList() : List<DownloadItem> fun getActiveAndPausedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status = 'Processing'") @Query("SELECT * FROM downloads WHERE status = 'Processing'")
@ -62,32 +59,31 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Active'") @Query("SELECT * FROM downloads WHERE status='Active'")
suspend fun getActiveDownloadsList() : List<DownloadItem> suspend fun getActiveDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused', 'Scheduled')") @Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')")
fun getActiveAndQueuedDownloadsList() : List<DownloadItem> fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@Query("UPDATE downloads SET status='Queued' where status in ('Active', 'QueuedPaused', 'ActivePaused')")
suspend fun resetActivePausedItems()
@Query("SELECT id FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused')") @Query("SELECT id FROM downloads WHERE status in('Active','Queued')")
fun getActiveAndQueuedDownloadIDs() : List<Long> fun getActiveAndQueuedDownloadIDs() : List<Long>
@Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused')") @Query("SELECT * FROM downloads WHERE status in('Active','Queued')")
fun getActiveAndQueuedDownloads() : Flow<List<DownloadItem>> fun getActiveAndQueuedDownloads() : Flow<List<DownloadItem>>
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY id") @Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY id")
fun getQueuedDownloads() : PagingSource<Int, DownloadItemSimple> fun getQueuedDownloads() : PagingSource<Int, DownloadItemSimple>
@Query("SELECT format FROM downloads WHERE status in('Queued','QueuedPaused')") @Query("SELECT format FROM downloads WHERE status='Queued'")
fun getSelectedFormatFromQueued() : List<Format> fun getSelectedFormatFromQueued() : List<Format>
@Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status in ('Queued', 'Scheduled') ORDER BY downloadStartTime, id LIMIT 20") @Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status in ('Queued', 'Scheduled') ORDER BY downloadStartTime, id LIMIT 20")
fun getQueuedScheduledDownloadsUntil(currentTime: Long) : Flow<List<DownloadItem>> fun getQueuedScheduledDownloadsUntil(currentTime: Long) : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status in ('Queued','QueuedPaused','ActivePaused') ORDER BY downloadStartTime, id") @Query("SELECT * FROM downloads WHERE status='Queued' 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> fun getQueuedDownloadsList() : List<DownloadItem>
@Query("SELECT id FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY id") @Query("SELECT id FROM downloads WHERE status='Queued' ORDER BY id")
fun getQueuedDownloadsListIDs() : List<Long> fun getQueuedDownloadsListIDs() : List<Long>
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
@ -133,22 +129,6 @@ interface DownloadDao {
@Query("SELECT status FROM downloads WHERE id=:id") @Query("SELECT status FROM downloads WHERE id=:id")
fun checkStatus(id: Long) : DownloadRepository.Status? fun checkStatus(id: Long) : DownloadRepository.Status?
@Query("UPDATE downloads " +
"SET status = CASE " +
" WHEN status = 'Active' THEN 'ActivePaused' " +
" WHEN status = 'Queued' THEN 'QueuedPaused' " +
" ELSE status " +
" END;")
fun pauseActiveAndQueued()
@Query("UPDATE downloads " +
"SET status = CASE " +
" WHEN status = 'ActivePaused' THEN 'Active' " +
" WHEN status = 'QueuedPaused' THEN 'Queued' " +
" ELSE status " +
" END;")
fun unPauseActiveAndQueued()
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(item: DownloadItem) : Long suspend fun insert(item: DownloadItem) : Long
@ -179,7 +159,7 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE id in (:list)") @Query("DELETE FROM downloads WHERE id in (:list)")
suspend fun deleteAllWithIDs(list: List<Long>) suspend fun deleteAllWithIDs(list: List<Long>)
@Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','QueuedPaused','Active','ActivePaused', 'Scheduled')") @Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','Active', 'Scheduled')")
suspend fun cancelActiveQueued() suspend fun cancelActiveQueued()
@Query("DELETE FROM downloads WHERE status='Processing' AND id=:id") @Query("DELETE FROM downloads WHERE status='Processing' AND id=:id")
@ -188,6 +168,9 @@ interface DownloadDao {
@Upsert @Upsert
suspend fun update(item: DownloadItem) suspend fun update(item: DownloadItem)
@Query("UPDATE downloads set status=:status where id=:id")
suspend fun setStatus(id: Long, status: String)
@Update @Update
suspend fun updateWithoutUpsert(item: DownloadItem) suspend fun updateWithoutUpsert(item: DownloadItem)
@ -233,14 +216,11 @@ interface DownloadDao {
@Query("UPDATE downloads SET downloadStartTime=0, status='Queued' where id in (:list)") @Query("UPDATE downloads SET downloadStartTime=0, status='Queued' where id in (:list)")
suspend fun resetScheduleTimeForItems(list: List<Long>) suspend fun resetScheduleTimeForItems(list: List<Long>)
@Query("UPDATE downloads SET status='Queued' where status in ('QueuedPaused', 'ActivePaused')")
suspend fun resetPausedItems()
@Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)") @Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)")
suspend fun reQueueDownloadItems(list: List<Long>) suspend fun reQueueDownloadItems(list: List<Long>)
@Query("Update downloads SET status='Saved' WHERE status='Processing'") @Query("Update downloads SET status='Saved' WHERE status='Processing' and id in (:ids)")
fun updateProcessingtoSavedStatus() fun updateProcessingtoSavedStatus(ids: List<Long>)
@Transaction @Transaction
suspend fun putAtTopOfTheQueue(existingIDs: List<Long>){ suspend fun putAtTopOfTheQueue(existingIDs: List<Long>){

View file

@ -26,6 +26,7 @@ import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.work.DownloadWorker import com.deniscerri.ytdl.work.DownloadWorker
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -59,17 +60,15 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
) )
val activeDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active).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).toListString())
val queuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Queued, Status.QueuedPaused).toListString()) val activeQueuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.Queued).toListString())
val activeQueuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.ActivePaused, Status.Queued, Status.QueuedPaused).toListString())
val cancelledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Cancelled).toListString()) val cancelledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Cancelled).toListString())
val erroredDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Error).toListString()) val erroredDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Error).toListString())
val savedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Saved).toListString()) val savedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Saved).toListString())
val pausedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.ActivePaused, Status.QueuedPaused).toListString())
val scheduledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Scheduled).toListString()) val scheduledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Scheduled).toListString())
enum class Status { enum class Status {
Active, ActivePaused, Queued, QueuedPaused, Error, Cancelled, Saved, Processing, Scheduled Active, ActivePaused, Queued, Error, Cancelled, Saved, Processing, Scheduled
} }
suspend fun insert(item: DownloadItem) : Long { suspend fun insert(item: DownloadItem) : Long {
@ -102,9 +101,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
} }
suspend fun setDownloadStatus(item: DownloadItem, status: Status){ suspend fun setDownloadStatus(id: Long, status: Status){
item.status = status.toString() downloadDao.setStatus(id, status.toString())
update(item)
} }
fun getItemByID(id: Long) : DownloadItem { fun getItemByID(id: Long) : DownloadItem {
@ -184,14 +182,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.cancelActiveQueued() downloadDao.cancelActiveQueued()
} }
fun pauseDownloads(){
downloadDao.pauseActiveAndQueued()
}
fun unPauseDownloads(){
downloadDao.unPauseActiveAndQueued()
}
fun removeLogID(logID: Long){ fun removeLogID(logID: Long){
downloadDao.removeLogID(logID) downloadDao.removeLogID(logID)
} }
@ -200,6 +190,13 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.removeAllLogID() downloadDao.removeAllLogID()
} }
fun getFilteredProcessingDownloads(ids: List<Long>) : Flow<List<DownloadItem>> {
return downloadDao.getProcessingDownloads()
.map {
it.filter { ids.contains(it.id) }
}
}
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) { suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) {

View file

@ -157,7 +157,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
private fun getQueryType(inputQuery: String) : SourceType { private fun getQueryType(inputQuery: String) : SourceType {
var type = SourceType.SEARCH_QUERY var type = SourceType.SEARCH_QUERY
val p = Pattern.compile("((^(https?)://)?(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)") val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val m = p.matcher(inputQuery) val m = p.matcher(inputQuery)
if (m.find()) { if (m.find()) {
type = SourceType.YOUTUBE_VIDEO type = SourceType.YOUTUBE_VIDEO

View file

@ -5,13 +5,10 @@ import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
@ -20,7 +17,6 @@ import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.App import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.DBManager
@ -41,21 +37,20 @@ import com.deniscerri.ytdl.ui.downloadcard.FormatTuple
import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker
import com.google.gson.Gson
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
@ -77,14 +72,12 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val scheduledDownloads : Flow<PagingData<DownloadItemSimple>> val scheduledDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloadsCount : Flow<Int> val activeDownloadsCount : Flow<Int>
val activeAndActivePausedDownloadsCount : Flow<Int>
val queuedDownloadsCount : Flow<Int> val queuedDownloadsCount : Flow<Int>
val activeQueuedDownloadsCount : Flow<Int> val activeQueuedDownloadsCount : Flow<Int>
val cancelledDownloadsCount : Flow<Int> val cancelledDownloadsCount : Flow<Int>
val erroredDownloadsCount : Flow<Int> val erroredDownloadsCount : Flow<Int>
val savedDownloadsCount : Flow<Int> val savedDownloadsCount : Flow<Int>
val scheduledDownloadsCount : Flow<Int> val scheduledDownloadsCount : Flow<Int>
val pausedDownloadsCount: Flow<Int>
val alreadyExistsUiState: MutableStateFlow<List<SharedDownloadViewModel.AlreadyExistsIDs>> val alreadyExistsUiState: MutableStateFlow<List<SharedDownloadViewModel.AlreadyExistsIDs>>
@ -130,14 +123,12 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
infoUtil = InfoUtil(application) infoUtil = InfoUtil(application)
activeDownloadsCount = repository.activeDownloadsCount activeDownloadsCount = repository.activeDownloadsCount
activeAndActivePausedDownloadsCount = repository.activeAndActivePausedDownloadsCount
queuedDownloadsCount = repository.queuedDownloadsCount queuedDownloadsCount = repository.queuedDownloadsCount
activeQueuedDownloadsCount = repository.activeQueuedDownloadsCount activeQueuedDownloadsCount = repository.activeQueuedDownloadsCount
cancelledDownloadsCount = repository.cancelledDownloadsCount cancelledDownloadsCount = repository.cancelledDownloadsCount
erroredDownloadsCount = repository.erroredDownloadsCount erroredDownloadsCount = repository.erroredDownloadsCount
savedDownloadsCount = repository.savedDownloadsCount savedDownloadsCount = repository.savedDownloadsCount
scheduledDownloadsCount = repository.scheduledDownloadsCount scheduledDownloadsCount = repository.scheduledDownloadsCount
pausedDownloadsCount = repository.pausedDownloadsCount
allDownloads = repository.allDownloads.flow allDownloads = repository.allDownloads.flow
queuedDownloads = repository.queuedDownloads.flow queuedDownloads = repository.queuedDownloads.flow
@ -294,8 +285,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val cropThumb = sharedPreferences.getBoolean("crop_thumbnail", false) val cropThumb = sharedPreferences.getBoolean("crop_thumbnail", false)
val customFileNameTemplate = when(historyItem.type) { val customFileNameTemplate = when(historyItem.type) {
Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)s - %(title)s") Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)30B - %(title)120B")
Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s") Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)30B - %(title)120B")
else -> "" else -> ""
} }
@ -371,78 +362,57 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
data class ProcessingItemsJob( data class ProcessingItemsJob(
var jobID: Long = 0,
var job: Job? = null, var job: Job? = null,
var itemType: String, var originItemType: String,
var itemIDs: List<Long> var originItemIDs: List<Long>,
var processingDownloadItemIDs: MutableList<Long> = mutableListOf()
) )
val loadingProcessingItems: MutableStateFlow<Boolean> = MutableStateFlow(false) var processingItemsJob: MutableLiveData<ProcessingItemsJob?> = MutableLiveData(null)
var loadingProcessingDownloadsJobs: MutableStateFlow<List<ProcessingItemsJob>> = MutableStateFlow(listOf()) var _filteredProcessingItems = MutableStateFlow<List<DownloadItem>>(listOf())
private fun cancelAllLoadingProcessingDownloads() { var filteredProcessingItems: StateFlow<List<DownloadItem>> = _filteredProcessingItems
loadingProcessingDownloadsJobs.value.onEach { private fun updateProcessingJobData(item: ProcessingItemsJob) = viewModelScope.launch {
it.job?.cancel(CancellationException()) processingItemsJob.postValue(item)
}
} }
fun getLoadingProcessingDownloadJob(jobID: Long) : ProcessingItemsJob? { fun turnDownloadItemsToProcessingDownloads(itemIDs: List<Long>) = viewModelScope.launch(Dispatchers.IO){
return loadingProcessingDownloadsJobs.value.firstOrNull { it.jobID == jobID } processingItemsJob.postValue(ProcessingItemsJob(null, DownloadItem::class.java.toString(), itemIDs))
}
fun cancelLoadingProcessingDownloads(jobID: Long) {
loadingProcessingDownloadsJobs.value.firstOrNull {it.jobID == jobID }?.apply {
this.job?.cancel(CancellationException())
}
}
fun turnDownloadItemsToProcessingDownloads(itemIDs: List<Long>) : Long {
val jobID = System.currentTimeMillis()
val job = viewModelScope.launch(Dispatchers.IO) { val job = viewModelScope.launch(Dispatchers.IO) {
val insertedIDs = mutableListOf<Long>()
try { try {
loadingProcessingItems.emit(true)
itemIDs.chunked(100).forEach { ids -> itemIDs.chunked(100).forEach { ids ->
val items = repository.getAllItemsByIDs(ids) val items = repository.getAllItemsByIDs(ids)
items.apply { if (!isActive) {
if (!isActive) { throw CancellationException()
throw CancellationException() }
} items.forEach {
this.forEach { it.id = 0
it.id = 0 it.status = DownloadRepository.Status.Processing.toString()
it.status = DownloadRepository.Status.Processing.toString() }
insertedIDs.add(repository.insert(it))
} processingItemsJob.value?.apply {
processingDownloadItemIDs.addAll(repository.insertAll(items))
updateProcessingJobData(this)
} }
} }
loadingProcessingItems.emit(false)
} catch (e: Exception) { } catch (e: Exception) {
repository.deleteAllWithIDs(insertedIDs) processingItemsJob.value?.apply {
loadingProcessingItems.emit(false) this.processingDownloadItemIDs.chunked(100).forEach {
repository.deleteAllWithIDs(it)
}
}
processingItemsJob.postValue(null)
} }
} }
processingItemsJob.postValue(ProcessingItemsJob(job, DownloadItem::class.java.toString(), itemIDs))
viewModelScope.launch(Dispatchers.IO) {
val currentJobs = loadingProcessingDownloadsJobs.value.toMutableList()
currentJobs.add(
ProcessingItemsJob(jobID, job, DownloadItem::class.java.toString(), itemIDs)
)
loadingProcessingDownloadsJobs.emit(currentJobs)
}
return jobID
} }
fun turnResultItemsToProcessingDownloads(itemIds: List<Long>, downloadNow: Boolean = false) : Long { fun turnResultItemsToProcessingDownloads(itemIDs: List<Long>, downloadNow: Boolean = false) = viewModelScope.launch(Dispatchers.IO) {
val jobID = System.currentTimeMillis() processingItemsJob.postValue(ProcessingItemsJob(null, ResultItem::class.java.toString(), itemIDs))
val job = viewModelScope.launch(Dispatchers.IO) { val job = viewModelScope.launch(Dispatchers.IO) {
val insertedIds = mutableListOf<Long>()
try { try {
loadingProcessingItems.emit(true) itemIDs.chunked(100).forEach { ids ->
itemIds.chunked(100).forEach { ids ->
val items = resultRepository.getAllByIDs(ids) val items = resultRepository.getAllByIDs(ids)
val downloadItems = items.map { val downloadItems = items.map {
val preferredType = getDownloadType(url = it.url).toString() val preferredType = getDownloadType(url = it.url).toString()
@ -462,41 +432,29 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
queueDownloads(downloadItems) queueDownloads(downloadItems)
}else{ }else{
downloadItems.forEach { processingItemsJob.value?.apply {
insertedIds.add(repository.insert(it)) processingDownloadItemIDs.addAll(repository.insertAll(downloadItems))
updateProcessingJobData(this)
} }
} }
} }
loadingProcessingItems.emit(false)
}catch (e: Exception) { }catch (e: Exception) {
repository.deleteAllWithIDs(insertedIds) processingItemsJob.value?.apply {
loadingProcessingItems.emit(false) this.processingDownloadItemIDs.chunked(100).forEach {
repository.deleteAllWithIDs(it)
}
}
processingItemsJob.postValue(null)
} }
} }
viewModelScope.launch(Dispatchers.IO) { processingItemsJob.postValue(ProcessingItemsJob(job, ResultItem::class.java.toString(), itemIDs))
val currentJobs = loadingProcessingDownloadsJobs.value.toMutableList()
currentJobs.add(
ProcessingItemsJob(jobID, job, ResultItem::class.java.toString(), itemIds)
)
loadingProcessingDownloadsJobs.emit(currentJobs)
}
return jobID
} }
fun insert(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){ fun insert(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){
repository.insert(item) repository.insert(item)
} }
fun pauseDownloads() = viewModelScope.launch(Dispatchers.IO){
repository.pauseDownloads()
}
fun unPauseDownloads() = viewModelScope.launch(Dispatchers.IO){
repository.unPauseDownloads()
}
fun insertAll(items: List<DownloadItem>)= viewModelScope.launch(Dispatchers.IO){ fun insertAll(items: List<DownloadItem>)= viewModelScope.launch(Dispatchers.IO){
items.forEach{ items.forEach{
repository.insert(it) repository.insert(it)
@ -540,7 +498,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) { fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) {
cancelAllLoadingProcessingDownloads() processingItemsJob.value?.apply { this.job?.cancel(CancellationException()) }
repository.cancelActiveQueued() repository.cancelActiveQueued()
} }
@ -563,8 +521,12 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return repository.getActiveDownloads() return repository.getActiveDownloads()
} }
fun getActiveAndQueuedDownloads() : List<DownloadItem>{ fun getActiveDownloadsCount() : Int {
return repository.getActiveAndQueuedDownloads() return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused).toListString())
}
fun getQueuedDownloadsCount() : Int {
return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Queued).toListString())
} }
fun getActiveAndQueuedDownloadIDs() : List<Long>{ fun getActiveAndQueuedDownloadIDs() : List<Long>{
@ -573,10 +535,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch { suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.resetScheduleTimeForItems(items) dbManager.downloadDao.resetScheduleTimeForItems(items)
dbManager.downloadDao.resetPausedItems()
repository.startDownloadWorker(emptyList(), application) repository.startDownloadWorker(emptyList(), application)
} }
suspend fun resetActivePaused() {
dbManager.downloadDao.resetActivePausedItems()
}
suspend fun startDownloadWorker(list: List<DownloadItem>){ suspend fun startDownloadWorker(list: List<DownloadItem>){
repository.startDownloadWorker(list, application) repository.startDownloadWorker(list, application)
} }
@ -664,36 +629,42 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dbManager.downloadDao.getDownloadIDsNotPresentInList(items.ifEmpty { listOf(-1L) }, status.map { it.toString() }) return dbManager.downloadDao.getDownloadIDsNotPresentInList(items.ifEmpty { listOf(-1L) }, status.map { it.toString() })
} }
fun moveProcessingToSavedCategory(){ fun moveProcessingToSavedCategory(ids: List<Long>){
dao.updateProcessingtoSavedStatus() ids.chunked(100).forEach {
dao.updateProcessingtoSavedStatus(it)
}
} }
suspend fun updateProcessingFormat(selectedFormats: List<FormatTuple>): List<Long> { suspend fun updateProcessingFormat(ids: List<Long>, selectedFormats: List<FormatTuple>): List<Long> {
val items = repository.getProcessingDownloads() return ids.chunked(100).map {
items.forEachIndexed { index, i -> val items = repository.getAllItemsByIDs(it)
selectedFormats[index].format?.apply { items.forEachIndexed { index, i ->
i.format = this selectedFormats[index].format?.apply {
i.format = this
}
if (i.type == Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
repository.update(i)
} }
if (i.type == Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
repository.update(i)
}
return items.map { it.format.filesize } items.map { itm -> itm.format.filesize }
}.flatten()
} }
suspend fun updateProcessingCommandFormat(format: Format){ suspend fun updateProcessingCommandFormat(ids: List<Long>, format: Format){
val items = repository.getProcessingDownloads() ids.chunked(100).forEach {
items.forEach { i -> repository.getAllItemsByIDs(it).forEach { i ->
i.format = format i.format = format
repository.update(i) repository.update(i)
}
} }
} }
suspend fun updateProcessingDownloadPath(path: String){ suspend fun updateProcessingDownloadPath(ids: List<Long>, path: String){
val items = repository.getProcessingDownloads() ids.chunked(100).forEach {
items.forEach { i -> repository.getAllItemsByIDs(it).forEach { i ->
i.downloadPath = path i.downloadPath = path
repository.update(i) repository.update(i)
}
} }
} }
@ -706,66 +677,73 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
suspend fun updateProcessingAllFormats(formatCollection: List<List<Format>>) { suspend fun updateProcessingAllFormats(ids: List<Long>, formatCollection: List<List<Format>>) {
val items = repository.getProcessingDownloads() ids.chunked(100).forEach {
items.forEachIndexed { index, i -> val items = repository.getAllItemsByIDs(it)
i.allFormats.clear() items.forEachIndexed { index, i ->
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) { i.allFormats.clear()
runCatching { if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) {
i.allFormats.addAll(formatCollection[index]) runCatching {
i.allFormats.addAll(formatCollection[index])
}
} }
} i.format = getFormat(i.allFormats, i.type)
i.format = getFormat(i.allFormats, i.type) kotlin.runCatching {
kotlin.runCatching { dbManager.resultDao.getResultByURL(i.url)?.apply {
dbManager.resultDao.getResultByURL(i.url)?.apply { this.formats = formatCollection[index].toMutableList()
this.formats = formatCollection[index].toMutableList() dbManager.resultDao.update(this)
dbManager.resultDao.update(this) }
} }
repository.update(i)
} }
repository.update(i)
} }
} }
suspend fun continueUpdatingFormatsOnBackground(){ suspend fun continueUpdatingFormatsOnBackground(itemIDs: List<Long>){
dao.getProcessingDownloadsList().apply { itemIDs.chunked(100).forEach {list ->
this.forEach { repository.getAllItemsByIDs(list).onEach {
it.status = DownloadRepository.Status.Saved.toString() it.status = DownloadRepository.Status.Saved.toString()
repository.update(it) repository.update(it)
} }
val ids = this.map { it.id }
val id = System.currentTimeMillis().toInt()
val workRequest = OneTimeWorkRequestBuilder<UpdatePlaylistFormatsWorker>()
.setInputData(
Data.Builder()
.putLongArray("ids", ids.toLongArray())
.putInt("id", id)
.build())
.addTag("updateFormats")
.build()
val context = App.instance
WorkManager.getInstance(context).enqueueUniqueWork(
id.toString(),
ExistingWorkPolicy.REPLACE,
workRequest
)
} }
val id = System.currentTimeMillis().toInt()
val workRequest = OneTimeWorkRequestBuilder<UpdatePlaylistFormatsWorker>()
.setInputData(
Data.Builder()
.putLongArray("ids", itemIDs.toLongArray())
.putInt("id", id)
.build())
.addTag("updateFormats")
.build()
val context = App.instance
WorkManager.getInstance(context).enqueueUniqueWork(
id.toString(),
ExistingWorkPolicy.REPLACE,
workRequest
)
} }
suspend fun updateProcessingType(newType: Type) { suspend fun updateProcessingType(ids: List<Long>, newType: Type) {
repository.getProcessingDownloads().apply { ids.chunked(100).forEach { _ ->
val new = switchDownloadType(this, newType) repository.getAllItemsByIDs(ids).apply {
new.forEach { val new = switchDownloadType(this, newType)
repository.update(it) new.forEach {
repository.update(it)
}
} }
} }
} }
fun checkIfAllProcessingItemsHaveSameType() : Pair<Boolean, Type> { fun checkIfAllProcessingItemsHaveSameType(ids: List<Long>) : Pair<Boolean, Type> {
val counts = dao.getProcessingDownloadsCountByType() val types = mutableSetOf<String>()
val sameType = counts.size == 1 || counts[0] == counts[1] ids.chunked(100).forEach {
val first = dao.getFirstProcessingDownload() types.addAll(dao.getProcessingDownloadTypes(it))
return Pair(sameType, first.type) }
return Pair(types.size == 1, Type.valueOf(types.first()))
} }
@ -775,6 +753,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val first = dao.getFirstProcessingDownload() val first = dao.getFirstProcessingDownload()
} }
suspend fun updateToStatus(id: Long, status: DownloadRepository.Status) {
repository.setDownloadStatus(id, status)
}
fun getURLsByStatus(list: List<DownloadRepository.Status>) : List<String> { fun getURLsByStatus(list: List<DownloadRepository.Status>) : List<String> {
return dao.getURLsByStatus(list.map { it.toString() }) return dao.getURLsByStatus(list.map { it.toString() })
} }

View file

@ -40,6 +40,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import okhttp3.internal.immutableListOf
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
@ -282,16 +283,32 @@ class SharedDownloadViewModel(private val context: Context) {
private fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> { private fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf() val requirements: MutableList<(Format) -> Int> = mutableListOf()
requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) 10 else 0}
sharedPreferences.getString("audio_language", "")?.apply { val itemValues = resources.getStringArray(R.array.format_importance_audio_values).toSet()
if (this.isNotBlank()){ val prefAudio = sharedPreferences.getString("format_importance_audio", itemValues.joinToString(","))!!
requirements.add { it: Format -> if (it.lang?.contains(this) == true) 3 else 0 }
prefAudio.split(",").forEachIndexed { idx, s ->
var importance = (itemValues.size - idx) * 10
when(s) {
"id" -> {
requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) importance else 0}
}
"language" -> {
sharedPreferences.getString("audio_language", "")?.apply {
if (this.isNotBlank()){
requirements.add { it: Format -> if (it.lang?.contains(this) == true) importance else 0 }
}
}
}
"codec" -> {
requirements.add {it: Format -> if ("^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)) importance else 0}
}
"container" -> {
requirements.add {it: Format -> if (it.container == audioContainer) importance else 0 }
}
} }
} }
requirements.add {it: Format -> if ("^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)) 2 else 0}
requirements.add {it: Format -> if (it.container == audioContainer) 1 else 0 }
return requirements return requirements
} }
@ -299,42 +316,61 @@ class SharedDownloadViewModel(private val context: Context) {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> { fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf() val requirements: MutableList<(Format) -> Int> = mutableListOf()
//format id
requirements.add { it: Format -> if (formatIDPreference.contains(it.format_id)) 20 else 0 } val itemValues = resources.getStringArray(R.array.format_importance_video_values).toSet()
//resolutions val prefVideo = sharedPreferences.getString("format_importance_video", itemValues.joinToString(","))!!
context.getStringArray(R.array.video_formats_values)
.filter { it.contains("_") } prefVideo.split(",").forEachIndexed { idx, s ->
.map{ it.split("_")[0].dropLast(1) val importance = (itemValues.size - idx) * 10
}.toMutableList().apply {
when(videoQualityPreference) { when(s) {
"worst" -> { "id" -> {
requirements.add { it: Format -> if (it.format_note.contains("worst", ignoreCase = true)) (15) else 0 } requirements.add { it: Format -> if (formatIDPreference.contains(it.format_id)) importance else 0 }
} }
"best" -> { "resolution" -> {
requirements.add { it: Format -> if (it.format_note.contains("best", ignoreCase = true)) (15) else 0 } context.getStringArray(R.array.video_formats_values)
} .filter { it.contains("_") }
else -> { .map{ it.split("_")[0].dropLast(1)
val preferenceIndex = this.indexOfFirst { videoQualityPreference.contains(it) } }.toMutableList().apply {
val preference = this[preferenceIndex] when(videoQualityPreference) {
for(i in 0..preferenceIndex){ "worst" -> {
removeAt(0) requirements.add { it: Format -> if (it.format_note.contains("worst", ignoreCase = true)) (importance) else 0 }
} }
add(0, preference) "best" -> {
forEachIndexed { index, res -> requirements.add { it: Format -> if (it.format_note.contains("best", ignoreCase = true)) (importance) else 0 }
requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (15 - index - 1) else 0 } }
else -> {
val preferenceIndex = this.indexOfFirst { videoQualityPreference.contains(it) }
val preference = this[preferenceIndex]
for(i in 0..preferenceIndex){
removeAt(0)
}
add(0, preference)
forEachIndexed { index, res ->
requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (importance - index - 1) else 0 }
}
}
}
} }
}
"codec" -> {
requirements.add { it: Format -> if ("^(${videoCodec})(.+)?$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)) importance else 0 }
}
"no_audio" -> {
requirements.add { it: Format -> if (it.acodec == "none" || it.acodec == "") importance else 0 }
}
"container" -> {
requirements.add { it: Format ->
if (videoContainer == "mp4")
if (it.container.equals("mpeg_4", true)) importance else 0
else
if (it.container.equals(videoContainer, true)) importance else 0
} }
} }
} }
requirements.add { it: Format -> if ("^(${videoCodec})(.+)?$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)) 5 else 0 }
requirements.add { it: Format -> if (it.acodec == "none" || it.acodec == "") 1 else 0 }
requirements.add { it: Format ->
if (videoContainer == "mp4")
if (it.container.equals("mpeg_4", true)) 1 else 0
else
if (it.container.equals(videoContainer, true)) 1 else 0
} }
return requirements return requirements
} }
@ -445,7 +481,7 @@ class SharedDownloadViewModel(private val context: Context) {
val downloadArchive = runCatching { File(FileUtil.getDownloadArchivePath(context)).useLines { it.toList() } }.getOrElse { listOf() } val downloadArchive = runCatching { File(FileUtil.getDownloadArchivePath(context)).useLines { it.toList() } }.getOrElse { listOf() }
.map { it.split(" ")[1] } .map { it.split(" ")[1] }
items.forEach { items.forEach {
if (! listOf(DownloadRepository.Status.ActivePaused, DownloadRepository.Status.Scheduled).toListString().contains(it.status)) if (it.status != DownloadRepository.Status.Scheduled.toString())
it.status = DownloadRepository.Status.Queued.toString() it.status = DownloadRepository.Status.Queued.toString()
var alreadyExists = false var alreadyExists = false
@ -584,7 +620,9 @@ class SharedDownloadViewModel(private val context: Context) {
} }
}else{ }else{
if (queuedItems.isNotEmpty()){ if (queuedItems.isNotEmpty()){
repository.startDownloadWorker(queuedItems, context) if (!sharedPreferences.getBoolean("paused_downloads", false)) {
repository.startDownloadWorker(queuedItems, context)
}
if(!useScheduler){ if(!useScheduler){
queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach { queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {

View file

@ -18,6 +18,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -43,39 +44,25 @@ class ProcessDownloadsInBackgroundService : Service() {
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val binding = intent.getBooleanExtra("binding", false)
if (binding) return super.onStartCommand(intent, flags, startId)
val notificationUtil = NotificationUtil(this) val notificationUtil = NotificationUtil(this)
startForeground(System.currentTimeMillis().toInt(), notificationUtil.createProcessingDownloads()) startForeground(System.currentTimeMillis().toInt(), notificationUtil.createProcessingDownloads())
val itemType = intent.getStringExtra("itemType") ?: ""
val itemIDs = intent.getLongArrayExtra("itemIDs") ?: longArrayOf() val itemType = intent.getStringExtra("itemType")!!
val jobData = DownloadViewModel.ProcessingItemsJob( val itemIDs = intent.getLongArrayExtra("itemIDs")!!.toList()
itemType = itemType, val processingItemIDs = intent.getLongArrayExtra("processingItemIDs")!!.toList()
itemIDs = itemIDs.toList()
)
val timeInMillis = intent.getLongExtra("timeInMillis", 0) val timeInMillis = intent.getLongExtra("timeInMillis", 0)
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { val processingFinished = intent.getBooleanExtra("processingFinished", true)
runJob(jobData, timeInMillis)
}
return super.onStartCommand(intent, flags, startId)
}
override fun onBind(intent: Intent): IBinder {
return binder
}
private fun DownloadItem.setAsScheduling(timeInMillis: Long) {
status = DownloadRepository.Status.Scheduled.toString()
downloadStartTime = timeInMillis
}
private suspend fun runJob(jobData: DownloadViewModel.ProcessingItemsJob, timeInMillis: Long = 0) {
val job = CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { val job = CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
if (jobData.itemType != "") { if (!processingFinished) {
val itemIDS = jobData.itemIDs when(itemType) {
val processingType = jobData.itemType
when(processingType) {
ResultItem::class.java.toString() -> { ResultItem::class.java.toString() -> {
itemIDS.chunked(100).map { ids -> itemIDs.chunked(100).map { ids ->
resultRepository.getAllByIDs(ids).map { resultRepository.getAllByIDs(ids).map {
downloadViewModel.createDownloadItemFromResult( downloadViewModel.createDownloadItemFromResult(
result = it, givenType = DownloadViewModel.Type.valueOf( result = it, givenType = DownloadViewModel.Type.valueOf(
@ -88,33 +75,42 @@ class ProcessDownloadsInBackgroundService : Service() {
it.setAsScheduling(timeInMillis) it.setAsScheduling(timeInMillis)
} }
} }
downloadViewModel.queueDownloads(this) if (isActive){
downloadViewModel.queueDownloads(this)
}
} }
} }
} }
DownloadItem::class.java.toString() -> { DownloadItem::class.java.toString() -> {
itemIDS.chunked(100).map { ids -> itemIDs.chunked(100).map { ids ->
repository.getAllItemsByIDs(ids).apply { repository.getAllItemsByIDs(ids).apply {
if (timeInMillis > 0) { if (timeInMillis > 0) {
this.forEach { this.forEach {
it.setAsScheduling(timeInMillis) it.setAsScheduling(timeInMillis)
} }
} }
downloadViewModel.queueDownloads(this) if (isActive){
downloadViewModel.queueDownloads(this)
}
} }
} }
} }
} }
}else { }else {
repository.getProcessingDownloads().apply { repository.getAllItemsByIDs(processingItemIDs).apply {
if (timeInMillis > 0){ this.chunked(100).map {
this.forEach { if (timeInMillis > 0){
it.setAsScheduling(timeInMillis) this.forEach { d ->
d.setAsScheduling(timeInMillis)
}
}
if (isActive){
downloadViewModel.queueDownloads(it)
} }
} }
downloadViewModel.queueDownloads(this)
} }
} }
} }
@ -126,6 +122,17 @@ class ProcessDownloadsInBackgroundService : Service() {
} }
} }
queueProcessingDownloadsJobList.add(job) queueProcessingDownloadsJobList.add(job)
return super.onStartCommand(intent, flags, startId)
}
override fun onBind(intent: Intent): IBinder {
return binder
}
private fun DownloadItem.setAsScheduling(timeInMillis: Long) {
status = DownloadRepository.Status.Scheduled.toString()
downloadStartTime = timeInMillis
} }
fun cancelAllProcessingJobs(){ fun cancelAllProcessingJobs(){

View file

@ -51,11 +51,13 @@ import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.search.SearchBar import com.google.android.material.search.SearchBar
import com.google.android.material.search.SearchView import com.google.android.material.search.SearchView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -176,12 +178,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
searchSuggestionsRecyclerView?.adapter = searchSuggestionsAdapter searchSuggestionsRecyclerView?.adapter = searchSuggestionsAdapter
searchSuggestionsRecyclerView?.itemAnimator = null searchSuggestionsRecyclerView?.itemAnimator = null
val progressBar = view.findViewById<View>(R.id.progress)
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
resultViewModel.items.observe(requireActivity()) { resultViewModel.items.observe(requireActivity()) {
kotlin.runCatching { kotlin.runCatching {
homeAdapter!!.submitList(it) homeAdapter!!.submitList(it)
resultsList = it resultsList = it
progressBar.isVisible = loadingItems && resultsList!!.isNotEmpty()
if(resultViewModel.repository.itemCount.value > 1 || resultViewModel.repository.itemCount.value == -1){ if(resultViewModel.repository.itemCount.value > 1 || resultViewModel.repository.itemCount.value == -1){
if (it.size > 1 && it[0].playlistTitle.isNotEmpty() && !loadingItems){ if (it.size > 1 && it[0].playlistTitle.isNotEmpty() && !loadingItems){
downloadAllFab!!.visibility = VISIBLE downloadAllFab!!.visibility = VISIBLE
@ -252,6 +256,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} }
loadingItems = res.processing loadingItems = res.processing
progressBar.isVisible = loadingItems && resultsList!!.isNotEmpty()
if (res.processing){ if (res.processing){
recyclerView?.setPadding(0,0,0,0) recyclerView?.setPadding(0,0,0,0)
shimmerCards!!.startShimmer() shimmerCards!!.startShimmer()
@ -303,11 +308,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
arguments?.remove("showDownloadsWithUpdatedFormats") arguments?.remove("showDownloadsWithUpdatedFormats")
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val ids = arguments?.getLongArray("downloadIds") ?: return@launch val ids = arguments?.getLongArray("downloadIds") ?: return@launch
val jobID = downloadViewModel.turnDownloadItemsToProcessingDownloads(ids.toList()) downloadViewModel.turnDownloadItemsToProcessingDownloads(ids.toList())
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf( findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
Pair("processingDownloadsJobID", jobID)
))
} }
} }
} }
@ -453,6 +456,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
searchBar!!.setOnMenuItemClickListener { m: MenuItem -> searchBar!!.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) { when (m.itemId) {
R.id.delete_results -> { R.id.delete_results -> {
resultsList = listOf()
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
resultViewModel.cancelParsingQueries() resultViewModel.cancelParsingQueries()
@ -733,11 +737,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
if (viewIdName.isNotEmpty()) { if (viewIdName.isNotEmpty()) {
if (viewIdName == "downloadAll") { if (viewIdName == "downloadAll") {
val showDownloadCard = sharedPreferences!!.getBoolean("download_card", true) val showDownloadCard = sharedPreferences!!.getBoolean("download_card", true)
val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(resultsList!!.map { it!!.id }, downloadNow = !showDownloadCard) downloadViewModel.turnResultItemsToProcessingDownloads(resultsList!!.map { it!!.id }, downloadNow = !showDownloadCard)
if (showDownloadCard){ if (showDownloadCard){
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf( findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
Pair("processingDownloadsJobID", jobID)
))
} }
} }
} }
@ -807,11 +809,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
downloadViewModel.getDownloadType(url = selectedObjects[0].url) downloadViewModel.getDownloadType(url = selectedObjects[0].url)
) )
}else{ }else{
val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(selectedObjects.map { it.id }, downloadNow = !showDownloadCard) downloadViewModel.turnResultItemsToProcessingDownloads(selectedObjects.map { it.id }, downloadNow = !showDownloadCard)
if (showDownloadCard){ if (showDownloadCard){
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf( findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
Pair("processingDownloadsJobID", jobID)
))
} }
} }
clearCheckedItems() clearCheckedItems()

View file

@ -11,6 +11,7 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@ -121,22 +122,27 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null) if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null)
cancelButton.setOnClickListener {onItemClickListener.onCancelClick(item.id)} cancelButton.setOnClickListener {onItemClickListener.onCancelClick(item.id)}
val activePaused = item.status == DownloadRepository.Status.ActivePaused.toString()
val resumeButton = card.findViewById<MaterialButton>(R.id.active_download_resume)
resumeButton.isVisible = activePaused
if (resumeButton.hasOnClickListeners()) resumeButton.setOnClickListener(null)
resumeButton.setOnClickListener {
resumeButton.isVisible = false
onItemClickListener.onResumeClick(item.id)
}
when(DownloadRepository.Status.valueOf(item.status)){ if (sharedPreferences.getBoolean("paused_downloads", false) || activePaused) {
DownloadRepository.Status.Active -> { progressBar.isIndeterminate = false
progressBar.isIndeterminate = true cancelButton.isEnabled = true
cancelButton.isEnabled = true output.text = activity.getString(R.string.exo_download_paused)
} }else{
DownloadRepository.Status.ActivePaused -> { progressBar.isIndeterminate = true
progressBar.isIndeterminate = false cancelButton.isEnabled = true
cancelButton.isEnabled = true
output.text = activity.getString(R.string.exo_download_paused)
}
else -> {}
} }
} }
interface OnItemClickListener { interface OnItemClickListener {
fun onCancelClick(itemID: Long) fun onCancelClick(itemID: Long)
fun onResumeClick(itemID: Long)
fun onOutputClick(item: DownloadItem) fun onOutputClick(item: DownloadItem)
} }

View file

@ -125,6 +125,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
else fileSize.text = fileSizeReadable else fileSize.text = fileSizeReadable
val menu = card.findViewById<View>(R.id.options) val menu = card.findViewById<View>(R.id.options)
val paused = sharedPreferences.getBoolean("paused_downloads", false) || item.status == DownloadRepository.Status.ActivePaused.toString()
menu.setOnClickListener { menu.setOnClickListener {
val popup = PopupMenu(activity, it) val popup = PopupMenu(activity, it)
popup.menuInflater.inflate(R.menu.active_downloads_minified, popup.menu) popup.menuInflater.inflate(R.menu.active_downloads_minified, popup.menu)
@ -133,7 +134,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
val pause = popup.menu[0] val pause = popup.menu[0]
val resume = popup.menu[1] val resume = popup.menu[1]
if (item.status == DownloadRepository.Status.ActivePaused.toString()){ if (paused){
pause.isVisible = false pause.isVisible = false
resume.isVisible = true resume.isVisible = true
}else{ }else{
@ -165,7 +166,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
} }
progressBar.isIndeterminate = item.status != DownloadRepository.Status.ActivePaused.toString() progressBar.isIndeterminate = !paused
card.setOnClickListener { card.setOnClickListener {
onItemClickListener.onCardClick() onItemClickListener.onCardClick()

View file

@ -78,7 +78,7 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
val author = card.findViewById<TextView>(R.id.author) val author = card.findViewById<TextView>(R.id.author)
author.text = video.author author.text = video.author
val duration = card.findViewById<TextView>(R.id.duration) val duration = card.findViewById<TextView>(R.id.duration)
if (video.duration.isNotEmpty()) { if (video.duration.isNotEmpty() && video.duration != "-1") {
duration.text = video.duration duration.text = video.duration
} }

View file

@ -7,6 +7,7 @@ import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.method.DigitsKeyListener
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
@ -144,7 +145,9 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
muteBtn = view.findViewById(R.id.mute) muteBtn = view.findViewById(R.id.mute)
rangeSlider = view.findViewById(R.id.rangeSlider) rangeSlider = view.findViewById(R.id.rangeSlider)
fromTextInput = view.findViewById(R.id.from_textinput_edittext) fromTextInput = view.findViewById(R.id.from_textinput_edittext)
fromTextInput.keyListener = DigitsKeyListener.getInstance("0123456789:.")
toTextInput = view.findViewById(R.id.to_textinput_edittext) toTextInput = view.findViewById(R.id.to_textinput_edittext)
toTextInput.keyListener = DigitsKeyListener.getInstance("0123456789:.")
cancelBtn = view.findViewById(R.id.cancelButton) cancelBtn = view.findViewById(R.id.cancelButton)
okBtn = view.findViewById(R.id.okButton) okBtn = view.findViewById(R.id.okButton)
suggestedChips = view.findViewById(R.id.chapters) suggestedChips = view.findViewById(R.id.chapters)

View file

@ -85,7 +85,21 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launch { lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO) { downloadItem = withContext(Dispatchers.IO) {
if (currentDownloadItem != null){ if (savedInstanceState?.containsKey("updated") == true) {
downloadItem.apply {
title = resultItem!!.title
author = resultItem!!.author
allFormats = resultItem!!.formats
format = downloadViewModel.getFormat(allFormats, Type.audio)
duration = resultItem!!.duration
playlistIndex = resultItem!!.playlistIndex
playlistURL = resultItem!!.playlistURL
playlistTitle = resultItem!!.playlistTitle
thumb = resultItem!!.thumb
website = resultItem!!.website
url = resultItem!!.url
}
}else if (currentDownloadItem != null){
//object cloning //object cloning
val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java) val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java) Gson().fromJson(string, DownloadItem::class.java)

View file

@ -323,6 +323,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
getAlsoAudioDownloadItem{ audioDownloadItem -> getAlsoAudioDownloadItem{ audioDownloadItem ->
audioDownloadItem.downloadStartTime = it.timeInMillis audioDownloadItem.downloadStartTime = it.timeInMillis
audioDownloadItem.status = DownloadRepository.Status.Scheduled.toString()
itemsToQueue.add(audioDownloadItem) itemsToQueue.add(audioDownloadItem)
runBlocking { runBlocking {

View file

@ -51,6 +51,8 @@ import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.Shimmer
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
@ -66,6 +68,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -73,6 +76,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.function.Predicate
class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), ConfigureMultipleDownloadsAdapter.OnItemClickListener, View.OnClickListener, class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), ConfigureMultipleDownloadsAdapter.OnItemClickListener, View.OnClickListener,
ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener { ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
@ -87,12 +91,17 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var bottomAppBar: BottomAppBar private lateinit var bottomAppBar: BottomAppBar
private lateinit var filesize : TextView private lateinit var filesize : TextView
private lateinit var count : TextView private lateinit var count : TextView
private lateinit var title: TextView
private lateinit var subtitle: TextView
private lateinit var shimmerTitle: ShimmerFrameLayout
private lateinit var shimmerSubtitle: ShimmerFrameLayout
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var currentDownloadIDs: List<Long> private lateinit var currentDownloadIDs: List<Long>
private var processingDownloadsJobID: Long = 0
private var processingItemsCount : Int = 0 private var processingItemsCount : Int = 0
private lateinit var jobData: DownloadViewModel.ProcessingItemsJob
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
@ -103,7 +112,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
infoUtil = InfoUtil(requireContext()) infoUtil = InfoUtil(requireContext())
currentDownloadIDs = arguments?.getLongArray("currentDownloadIDs")?.toList() ?: listOf() currentDownloadIDs = arguments?.getLongArray("currentDownloadIDs")?.toList() ?: listOf()
processingDownloadsJobID = arguments?.getLong("processingDownloadsJobID") ?: 0
processingItemsCount = currentDownloadIDs.size processingItemsCount = currentDownloadIDs.size
} }
@ -150,14 +158,76 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button) val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
val download = view.findViewById<Button>(R.id.bottomsheet_download_button) val download = view.findViewById<Button>(R.id.bottomsheet_download_button)
val progressBar = view.findViewById<LinearProgressIndicator>(R.id.loadingItemsProgress) bottomAppBar = view.findViewById(R.id.bottomAppBar)
val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type)
filesize = view.findViewById(R.id.filesize) filesize = view.findViewById(R.id.filesize)
count = view.findViewById(R.id.count) count = view.findViewById(R.id.count)
val continueInBackgroundSnackBar = Snackbar.make(recyclerView, R.string.process_downloads_background, Snackbar.LENGTH_LONG)
val snackbarView: View = continueInBackgroundSnackBar.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
title = view.findViewById(R.id.bottom_sheet_title)
subtitle = view.findViewById(R.id.bottom_sheet_subtitle)
shimmerTitle = view.findViewById(R.id.shimmer_loading_title)
shimmerSubtitle = view.findViewById(R.id.shimmer_loading_subtitle)
downloadViewModel.processingItemsJob.observe(this) {
it?.apply {
lifecycleScope.launch {
jobData = it
processingItemsCount = it.processingDownloadItemIDs.size
count.text = "${processingItemsCount} ${getString(R.string.selected)}"
val loading = it.originItemIDs.size != processingItemsCount
toggleLoadingShimmerTitle(loading)
bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading }
if (!loading){
continueInBackgroundSnackBar.dismiss()
}
downloadViewModel.processingDownloads.map { p -> p.filter { pd -> jobData.processingDownloadItemIDs.contains(pd.id) } }.collectLatest { items ->
listAdapter.submitList(items)
updateFileSize(items.map { it2 -> it2.format.filesize })
if (items.isNotEmpty()){
if (items.all { it2 -> it2.type == items[0].type }) {
bottomAppBar.menu[1].icon?.alpha = 255
if (items[0].type != DownloadViewModel.Type.command) {
bottomAppBar.menu[3].icon?.alpha = 255
}
} else {
bottomAppBar.menu[1].icon?.alpha = 30
bottomAppBar.menu[3].icon?.alpha = 30
}
val type = items.first().type
when(type){
DownloadViewModel.Type.audio -> {
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
}
DownloadViewModel.Type.video -> {
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
}
DownloadViewModel.Type.command -> {
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
}
else -> {}
}
}
}
}
}
}
scheduleBtn.setOnClickListener{ scheduleBtn.setOnClickListener{
fun initSchedule() { fun initSchedule(processingFinished: Boolean) {
UiUtil.showDatePicker(parentFragmentManager) { cal -> UiUtil.showDatePicker(parentFragmentManager) { cal ->
scheduleBtn.isEnabled = false scheduleBtn.isEnabled = false
download.isEnabled = false download.isEnabled = false
@ -167,11 +237,9 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
downloadViewModel.deleteAllWithID(currentDownloadIDs) downloadViewModel.deleteAllWithID(currentDownloadIDs)
} }
val jobData = withContext(Dispatchers.IO){ jobData.job?.cancel(CancellationException())
downloadViewModel.getLoadingProcessingDownloadJob(processingDownloadsJobID) jobData.job = null
} downloadProcessDownloadsInBackground(jobData, processingFinished)
jobData?.job?.cancel(CancellationException())
downloadProcessDownloadsInBackground(jobData, cal.timeInMillis)
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(cal.timeInMillis) val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(cal.timeInMillis)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show() Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
@ -181,22 +249,18 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
} }
if (progressBar.isVisible){ if (shimmerTitle.isVisible){
val continueInBackgroundSnackBar = Snackbar.make(view, R.string.process_downloads_background, Snackbar.LENGTH_LONG)
val snackbarView: View = continueInBackgroundSnackBar.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
continueInBackgroundSnackBar.setAction(R.string.ok) { continueInBackgroundSnackBar.setAction(R.string.ok) {
initSchedule() initSchedule(false)
} }
continueInBackgroundSnackBar.show() continueInBackgroundSnackBar.show()
}else{ }else{
initSchedule() initSchedule(true)
} }
} }
download!!.setOnClickListener { download!!.setOnClickListener {
fun initDownload() { fun initDownload(processingFinished: Boolean) {
scheduleBtn.isEnabled = false scheduleBtn.isEnabled = false
download.isEnabled = false download.isEnabled = false
lifecycleScope.launch { lifecycleScope.launch {
@ -204,26 +268,20 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
downloadViewModel.deleteAllWithID(currentDownloadIDs) downloadViewModel.deleteAllWithID(currentDownloadIDs)
} }
val jobData = withContext(Dispatchers.IO){ jobData.job?.cancel(CancellationException())
downloadViewModel.getLoadingProcessingDownloadJob(processingDownloadsJobID) jobData.job = null
} downloadProcessDownloadsInBackground(jobData, processingFinished)
jobData?.job?.cancel(CancellationException())
downloadProcessDownloadsInBackground(jobData)
dismiss() dismiss()
} }
} }
if (progressBar.isVisible){ if (shimmerTitle.isVisible){
val continueInBackgroundSnackBar = Snackbar.make(view, R.string.process_downloads_background, Snackbar.LENGTH_LONG)
val snackbarView: View = continueInBackgroundSnackBar.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
continueInBackgroundSnackBar.setAction(R.string.ok) { continueInBackgroundSnackBar.setAction(R.string.ok) {
initDownload() initDownload(false)
} }
continueInBackgroundSnackBar.show() continueInBackgroundSnackBar.show()
}else{ }else{
initDownload() initDownload(true)
} }
@ -237,8 +295,10 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch{ lifecycleScope.launch{
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs) downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.moveProcessingToSavedCategory() downloadViewModel.moveProcessingToSavedCategory(jobData.processingDownloadItemIDs)
} }
jobData.job?.cancel(CancellationException())
jobData.job = null
dismiss() dismiss()
} }
} }
@ -246,19 +306,16 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
true true
} }
bottomAppBar = view.findViewById(R.id.bottomAppBar)
val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type)
val formatListener = object : OnFormatClickListener { val formatListener = object : OnFormatClickListener {
override fun onFormatClick(selectedFormats: List<FormatTuple>) { override fun onFormatClick(selectedFormats: List<FormatTuple>) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingFormat(selectedFormats) downloadViewModel.updateProcessingFormat(jobData.processingDownloadItemIDs, selectedFormats)
} }
} }
override fun onFormatsUpdated(allFormats: List<List<Format>>) { override fun onFormatsUpdated(allFormats: List<List<Format>>) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingAllFormats(allFormats) downloadViewModel.updateProcessingAllFormats(jobData.processingDownloadItemIDs, allFormats)
} }
} }
@ -266,8 +323,10 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
override fun onContinueOnBackground() { override fun onContinueOnBackground() {
requireActivity().lifecycleScope.launch { requireActivity().lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.continueUpdatingFormatsOnBackground() downloadViewModel.continueUpdatingFormatsOnBackground(jobData.processingDownloadItemIDs)
} }
jobData.job?.cancel(CancellationException())
jobData.job = null
dismiss() dismiss()
} }
} }
@ -305,7 +364,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
audio!!.setOnClickListener { audio!!.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingType(DownloadViewModel.Type.audio) downloadViewModel.updateProcessingType(jobData.processingDownloadItemIDs, DownloadViewModel.Type.audio)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24) preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
bottomAppBar.menu[1].icon?.alpha = 255 bottomAppBar.menu[1].icon?.alpha = 255
@ -317,7 +376,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
video!!.setOnClickListener { video!!.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch{ CoroutineScope(Dispatchers.IO).launch{
downloadViewModel.updateProcessingType(DownloadViewModel.Type.video) downloadViewModel.updateProcessingType(jobData.processingDownloadItemIDs, DownloadViewModel.Type.video)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24) preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
bottomAppBar.menu[1].icon?.alpha = 255 bottomAppBar.menu[1].icon?.alpha = 255
@ -329,7 +388,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
command!!.setOnClickListener { command!!.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch{ CoroutineScope(Dispatchers.IO).launch{
downloadViewModel.updateProcessingType(DownloadViewModel.Type.command) downloadViewModel.updateProcessingType(jobData.processingDownloadItemIDs, DownloadViewModel.Type.command)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24) preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
bottomAppBar.menu[1].icon?.alpha = 255 bottomAppBar.menu[1].icon?.alpha = 255
@ -353,7 +412,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
R.id.format -> { R.id.format -> {
lifecycleScope.launch { lifecycleScope.launch {
val res = withContext(Dispatchers.IO){ val res = withContext(Dispatchers.IO){
downloadViewModel.checkIfAllProcessingItemsHaveSameType() downloadViewModel.checkIfAllProcessingItemsHaveSameType(jobData.processingDownloadItemIDs,)
} }
if (!res.first){ if (!res.first){
Toast.makeText(requireContext(), getString(R.string.format_filtering_hint), Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), getString(R.string.format_filtering_hint), Toast.LENGTH_SHORT).show()
@ -372,12 +431,12 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
) )
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingCommandFormat(format) downloadViewModel.updateProcessingCommandFormat(jobData.processingDownloadItemIDs, format)
} }
} }
}else{ }else{
val items = withContext(Dispatchers.IO){ val items = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloads() downloadViewModel.getAllByIDs(jobData.processingDownloadItemIDs)
} }
val flatFormatCollection = items.map { it.allFormats }.flatten() val flatFormatCollection = items.map { it.allFormats }.flatten()
val commonFormats = withContext(Dispatchers.IO){ val commonFormats = withContext(Dispatchers.IO){
@ -401,7 +460,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
R.id.more -> { R.id.more -> {
lifecycleScope.launch { lifecycleScope.launch {
val res = withContext(Dispatchers.IO){ val res = withContext(Dispatchers.IO){
downloadViewModel.checkIfAllProcessingItemsHaveSameType() downloadViewModel.checkIfAllProcessingItemsHaveSameType(jobData.processingDownloadItemIDs)
} }
if (!res.first) { if (!res.first) {
Toast.makeText( Toast.makeText(
@ -422,7 +481,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding) sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding)
val items = withContext(Dispatchers.IO){ val items = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloads() downloadViewModel.getAllByIDs(jobData.processingDownloadItemIDs)
} }
UiUtil.configureAudio( UiUtil.configureAudio(
@ -503,7 +562,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding) sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding)
val items = withContext(Dispatchers.IO){ val items = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloads() downloadViewModel.getAllByIDs(jobData.processingDownloadItemIDs)
} }
UiUtil.configureVideo( UiUtil.configureVideo(
@ -598,64 +657,31 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
true true
} }
lifecycleScope.launch { }
downloadViewModel.loadingProcessingDownloadsJobs.collectLatest { jobs ->
processingItemsCount = jobs.firstOrNull { j -> j.jobID == processingDownloadsJobID }?.itemIDs?.size ?: currentDownloadIDs.size
lifecycleScope.launch {
downloadViewModel.processingDownloads.collectLatest {
count.text = "${it.size} ${getString(R.string.selected)}"
val loading = it.size != processingItemsCount private fun toggleLoadingShimmerTitle(show: Boolean) {
progressBar.isVisible = loading shimmerTitle.isVisible = show
bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading } title.isVisible = !show
shimmerSubtitle.isVisible = show
subtitle.isVisible = !show
listAdapter.submitList(it) if (show){
withContext(Dispatchers.Main){ shimmerTitle.startShimmer()
updateFileSize(it.map { it2 -> it2.format.filesize }) shimmerSubtitle.startShimmer()
} }else{
shimmerTitle.stopShimmer()
if (it.isNotEmpty()){ shimmerSubtitle.stopShimmer()
if (it.all { it2 -> it2.type == it[0].type }) {
withContext(Dispatchers.Main) {
bottomAppBar.menu[1].icon?.alpha = 255
if (it[0].type != DownloadViewModel.Type.command) {
bottomAppBar.menu[3].icon?.alpha = 255
}
}
} else {
bottomAppBar.menu[1].icon?.alpha = 30
bottomAppBar.menu[3].icon?.alpha = 30
}
val type = it.first().type
when(type){
DownloadViewModel.Type.audio -> {
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
}
DownloadViewModel.Type.video -> {
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
}
DownloadViewModel.Type.command -> {
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
}
else -> {}
}
}
}
}
}
} }
} }
private fun updateFileSize(items: List<Long>){ private fun updateFileSize(items: List<Long>){
if (items.all { it > 0L }){ if (items.all { it > 5L }){
filesize.visibility = View.VISIBLE val size = FileUtil.convertFileSize(items.sum())
filesize.text = "${getString(R.string.file_size)}: >~ ${FileUtil.convertFileSize(items.sum())}" if (size != "?"){
filesize.visibility = View.VISIBLE
filesize.text = "${getString(R.string.file_size)}: >~ $size"
}
}else{ }else{
filesize.visibility = View.GONE filesize.visibility = View.GONE
} }
@ -674,7 +700,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingDownloadPath(result.data?.data.toString()) downloadViewModel.updateProcessingDownloadPath(jobData.processingDownloadItemIDs, result.data?.data.toString())
} }
val path = FileUtil.formatPath(result.data!!.data.toString()) val path = FileUtil.formatPath(result.data!!.data.toString())
@ -802,13 +828,14 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item) downloadViewModel.updateDownload(item)
} }
listAdapter.notifyDataSetChanged()
} }
} }
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {
downloadViewModel.cancelLoadingProcessingDownloads(processingDownloadsJobID) if (jobData.job != null){
downloadViewModel.deleteProcessing() jobData.job?.cancel(CancellationException())
downloadViewModel.deleteAllWithID(jobData.processingDownloadItemIDs)
}
super.onDismiss(dialog) super.onDismiss(dialog)
} }
@ -889,14 +916,13 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
} }
private fun downloadProcessDownloadsInBackground(jobData: DownloadViewModel.ProcessingItemsJob?, timeInMillis: Long = 0){ private fun downloadProcessDownloadsInBackground(jobData: DownloadViewModel.ProcessingItemsJob, processingFinished: Boolean, timeInMillis: Long = 0){
Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent -> Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent ->
jobData?.apply { intent.putExtra("itemType", jobData.originItemType)
intent.putExtra("itemType", jobData.itemType) intent.putExtra("itemIDs", jobData.originItemIDs.toLongArray())
intent.putExtra("itemIDs", jobData.itemIDs.toLongArray()) intent.putExtra("processingItemIDs", jobData.processingDownloadItemIDs.toLongArray())
intent.putExtra("timeInMillis", timeInMillis) intent.putExtra("timeInMillis", timeInMillis)
} intent.putExtra("processingFinished", processingFinished)
requireActivity().startService(intent) requireActivity().startService(intent)
} }
} }

View file

@ -88,7 +88,21 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch { lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO){ downloadItem = withContext(Dispatchers.IO){
if (currentDownloadItem != null){ if (savedInstanceState?.containsKey("updated") == true) {
downloadItem.apply {
title = resultItem!!.title
author = resultItem!!.author
allFormats = resultItem!!.formats
format = downloadViewModel.getFormat(allFormats, Type.video)
duration = resultItem!!.duration
playlistIndex = resultItem!!.playlistIndex
playlistURL = resultItem!!.playlistURL
playlistTitle = resultItem!!.playlistTitle
thumb = resultItem!!.thumb
website = resultItem!!.website
url = resultItem!!.url
}
}else if (currentDownloadItem != null){
val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java) val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java) Gson().fromJson(string, DownloadItem::class.java)
}else{ }else{

View file

@ -179,6 +179,7 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
try{ try{
//simple download //simple download
if (items.size == 1) { if (items.size == 1) {
formatCollection.clear()
kotlin.runCatching { kotlin.runCatching {
val res = withContext(Dispatchers.IO){ val res = withContext(Dispatchers.IO){
infoUtil.getFormats(items.first()!!.url) infoUtil.getFormats(items.first()!!.url)
@ -195,6 +196,7 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
listener.onFormatsUpdated(formats) listener.onFormatsUpdated(formats)
} }
formatCollection.add(res)
}.onFailure { err -> }.onFailure { err ->
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
UiUtil.handleResultResponse(requireActivity(), ResultViewModel.ResultsUiState( UiUtil.handleResultResponse(requireActivity(), ResultViewModel.ResultsUiState(

View file

@ -186,11 +186,9 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
)) ))
} }
}else{ }else{
val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(checkedResultItems.map { it!!.id }) downloadViewModel.turnResultItemsToProcessingDownloads(checkedResultItems.map { it.id })
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadMultipleBottomSheetDialog, findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadMultipleBottomSheetDialog)
bundleOf(Pair("processingDownloadsJobID", jobID))
)
} }
} }

View file

@ -2,21 +2,13 @@ package com.deniscerri.ytdl.ui.downloads
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity 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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -24,38 +16,30 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkQuery import androidx.work.WorkQuery
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdl.ui.adapter.QueuedDownloadAdapter
import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.work.DownloadWorker
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.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener { class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener {
@ -71,6 +55,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
private lateinit var resume: ExtendedFloatingActionButton private lateinit var resume: ExtendedFloatingActionButton
private lateinit var noResults: RelativeLayout private lateinit var noResults: RelativeLayout
private lateinit var workManager: WorkManager private lateinit var workManager: WorkManager
private lateinit var preferences: SharedPreferences
override fun onCreateView( override fun onCreateView(
@ -90,6 +75,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
notificationUtil = NotificationUtil(requireContext()) notificationUtil = NotificationUtil(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
workManager = WorkManager.getInstance(requireContext()) workManager = WorkManager.getInstance(requireContext())
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
activeDownloads = ActiveDownloadAdapter(this,requireActivity()) activeDownloads = ActiveDownloadAdapter(this,requireActivity())
activeRecyclerView = view.findViewById(R.id.download_recyclerview) activeRecyclerView = view.findViewById(R.id.download_recyclerview)
@ -105,102 +91,45 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
pause.setOnClickListener { pause.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
workManager.cancelAllWorkByTag("download") 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){ withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads() downloadViewModel.getActiveDownloads()
}.forEach { }.forEach {
cancelItem(it.id.toInt()) YoutubeDL.getInstance().destroyProcessById(it.id.toString())
it.status = DownloadRepository.Status.ActivePaused.toString()
downloadViewModel.updateDownload(it)
} }
preferences.edit().putBoolean("paused_downloads", true).apply()
activeDownloads.notifyDataSetChanged() activeDownloads.notifyDataSetChanged()
pause.isEnabled = true resume.isVisible = true
pause.isVisible = false
} }
} }
resume.setOnClickListener { resume.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
resume.isEnabled = false preferences.edit().putBoolean("paused_downloads", false).apply()
resume.isVisible = false
val active = withContext(Dispatchers.IO){ pause.isVisible = true
downloadViewModel.getActiveDownloads() withContext(Dispatchers.IO) {
} downloadViewModel.resetActivePaused()
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.startDownloadWorker(listOf()) downloadViewModel.startDownloadWorker(listOf())
} withContext(Dispatchers.Main){
activeDownloads.notifyDataSetChanged()
val queuedItems = withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}
queuedItems.map {
it.status = DownloadRepository.Status.Queued.toString()
downloadViewModel.updateDownload(it)
}
resume.isEnabled = true
}
}
WorkManager.getInstance(requireContext())
.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING))
.observe(viewLifecycleOwner){ list ->
list.forEach {work ->
if (work == null) return@forEach
val id = work.progress.getLong("id", 0L)
if(id == 0L) return@forEach
val progress = work.progress.getInt("progress", 0)
val output = work.progress.getString("output")
val progressBar = view.findViewWithTag<LinearProgressIndicator>("$id##progress")
val outputText = view.findViewWithTag<TextView>("$id##output")
requireActivity().runOnUiThread {
try {
progressBar?.setProgressCompat(progress, true)
outputText?.text = output
}catch (ignored: Exception) {}
} }
} }
} }
lifecycleScope.launch {
downloadViewModel.activeAndActivePausedDownloadsCount.collectLatest {
noResults.isVisible = it == 0
}
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.activeDownloadsCount.collectLatest { downloadViewModel.activeDownloadsCount.collectLatest {
pause.isVisible = it > 0 noResults.isVisible = it == 0
val pausedDownloads = preferences.getBoolean("paused_downloads", false)
pause.isVisible = it > 0 && !pausedDownloads
} }
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.pausedDownloadsCount.collectLatest { downloadViewModel.activeQueuedDownloadsCount.collectLatest {
resume.isVisible = it > 0 val pausedDownloads = preferences.getBoolean("paused_downloads", false)
resume.isVisible = it > 0 && pausedDownloads
} }
} }
@ -212,27 +141,79 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
} }
} }
@Subscribe(threadMode = ThreadMode.MAIN)
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) {
val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress")
val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output")
requireActivity().runOnUiThread {
try {
progressBar?.setProgressCompat(event.progress, true)
outputText?.text = event.output
}catch (ignored: Exception) {}
}
}
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
}
override fun onStop() {
super.onStop()
EventBus.getDefault().unregister(this)
}
override fun onCancelClick(itemID: Long) { override fun onCancelClick(itemID: Long) {
lifecycleScope.launch { lifecycleScope.launch {
val count = withContext(Dispatchers.IO){ YoutubeDL.getInstance().destroyProcessById(itemID.toString())
downloadViewModel.getActiveDownloads() notificationUtil.cancelDownloadNotification(itemID.toInt())
}.count()
if (count == 1){ val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
item.status = DownloadRepository.Status.Cancelled.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
val activeCount = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloadsCount()
}
if (activeCount == 0){
val queuedCount = withContext(Dispatchers.IO){
downloadViewModel.getQueuedDownloadsCount()
}
if (queuedCount == 0) {
preferences.edit().putBoolean("paused_downloads", false).apply()
}
}
if (activeCount == 1){
val queue = withContext(Dispatchers.IO){ val queue = withContext(Dispatchers.IO){
val list = downloadViewModel.getQueued().toMutableList() downloadViewModel.getQueued()
list.map { it.status = DownloadRepository.Status.Queued.toString() }
list
} }
runBlocking { if (!preferences.getBoolean("paused_downloads", false)){
downloadViewModel.queueDownloads(queue) runBlocking {
downloadViewModel.queueDownloads(queue)
}
} }
} }
} }
}
cancelDownload(itemID) override fun onResumeClick(itemID: Long) {
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
withContext(Dispatchers.IO){
downloadViewModel.queueDownloads(listOf(item))
}
}
} }
override fun onOutputClick(item: DownloadItem) { override fun onOutputClick(item: DownloadItem) {
@ -246,22 +227,4 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
} }
} }
private fun cancelItem(id: Int){
YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id)
}
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)
}
}
}
}
} }

View file

@ -130,15 +130,17 @@ class DownloadQueueMainFragment : Fragment(){
val reconfigureID = arguments?.getLong("reconfigure") val reconfigureID = arguments?.getLong("reconfigure")
reconfigureID?.apply { reconfigureID?.apply {
lifecycleScope.launch { lifecycleScope.launch {
val item = withContext(Dispatchers.IO){ kotlin.runCatching {
downloadViewModel.getItemByID(reconfigureID) val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(reconfigureID)
}
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
Pair("downloadItem", item),
Pair("result", downloadViewModel.createResultItemFromDownload(item)),
Pair("type", item.type)
)
)
} }
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
Pair("downloadItem", item),
Pair("result", downloadViewModel.createResultItemFromDownload(item)),
Pair("type", item.type)
)
)
} }
} }
@ -227,8 +229,8 @@ class DownloadQueueMainFragment : Fragment(){
R.id.copy_urls -> { R.id.copy_urls -> {
lifecycleScope.launch { lifecycleScope.launch {
val tabStatus = mapOf( val tabStatus = mapOf(
0 to listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused), 0 to listOf(DownloadRepository.Status.Active),
1 to listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused), 1 to listOf(DownloadRepository.Status.Queued),
2 to listOf(DownloadRepository.Status.Scheduled), 2 to listOf(DownloadRepository.Status.Scheduled),
3 to listOf(DownloadRepository.Status.Cancelled), 3 to listOf(DownloadRepository.Status.Cancelled),
4 to listOf(DownloadRepository.Status.Error), 4 to listOf(DownloadRepository.Status.Error),
@ -263,11 +265,10 @@ class DownloadQueueMainFragment : Fragment(){
} }
private fun cancelAllDownloads() { private fun cancelAllDownloads() {
cancelBackgroundProcessingDownloads()
workManager.cancelAllWorkByTag("download") workManager.cancelAllWorkByTag("download")
lifecycleScope.launch { lifecycleScope.launch {
val notificationUtil = NotificationUtil(requireContext()) val notificationUtil = NotificationUtil(requireContext())
downloadViewModel.cancelActiveQueued()
cancelBackgroundProcessingDownloads()
val activeAndQueued = withContext(Dispatchers.IO){ val activeAndQueued = withContext(Dispatchers.IO){
downloadViewModel.getActiveAndQueuedDownloadIDs() downloadViewModel.getActiveAndQueuedDownloadIDs()
} }
@ -275,6 +276,7 @@ class DownloadQueueMainFragment : Fragment(){
YoutubeDL.getInstance().destroyProcessById(id.toString()) YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id.toInt()) notificationUtil.cancelDownloadNotification(id.toInt())
} }
downloadViewModel.cancelActiveQueued()
} }
} }
@ -296,6 +298,7 @@ class DownloadQueueMainFragment : Fragment(){
} }
Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent -> Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent ->
intent.putExtra("binding", true)
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE) requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
} }
} }

View file

@ -129,7 +129,7 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
} }
} }
downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused)).observe(viewLifecycleOwner){ downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Queued)).observe(viewLifecycleOwner){
totalSize = it totalSize = it
noResults.isVisible = it == 0 noResults.isVisible = it == 0
dragHandleToggle.isVisible = it > 1 dragHandleToggle.isVisible = it > 1
@ -190,7 +190,7 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
val selectedIDs = getSelectedIDs().sortedBy { it } val selectedIDs = getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){ val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf( downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused).toListString()) DownloadRepository.Status.Queued).toListString())
}.toMutableList() }.toMutableList()
idsInMiddle.addAll(selectedIDs) idsInMiddle.addAll(selectedIDs)
if (idsInMiddle.isNotEmpty()){ if (idsInMiddle.isNotEmpty()){
@ -294,7 +294,7 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
return if (adapter.inverted || adapter.checkedItems.isEmpty()){ return if (adapter.inverted || adapter.checkedItems.isEmpty()){
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems.toList(), listOf( downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems.toList(), listOf(
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused)) DownloadRepository.Status.Queued))
} }
}else{ }else{
adapter.checkedItems.toList() adapter.checkedItems.toList()
@ -475,12 +475,18 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
} }
}, },
longClickDownloadButton = { longClickDownloadButton = {
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf( lifecycleScope.launch {
Pair("downloadItem", it), it.status = DownloadRepository.Status.Saved.toString()
Pair("result", downloadViewModel.createResultItemFromDownload(it)), withContext(Dispatchers.IO){
Pair("type", it.type) downloadViewModel.updateToStatus(it.id, DownloadRepository.Status.Saved)
) }
) findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
Pair("downloadItem", it),
Pair("result", downloadViewModel.createResultItemFromDownload(it)),
Pair("type", it.type)
)
)
}
}, },
scheduleButtonClick = {downloadItem -> scheduleButtonClick = {downloadItem ->
UiUtil.showDatePicker(parentFragmentManager) { UiUtil.showDatePicker(parentFragmentManager) {
@ -517,7 +523,7 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
if(selectedObjects == 2){ if(selectedObjects == 2){
val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it } val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){ val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused).toListString()) downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Queued).toListString())
} }
this.menu.findItem(R.id.select_between).isVisible = idsInMiddle.isNotEmpty() this.menu.findItem(R.id.select_between).isVisible = idsInMiddle.isNotEmpty()
} }

View file

@ -99,6 +99,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
topAppBar.setOnMenuItemClickListener { m: MenuItem -> topAppBar.setOnMenuItemClickListener { m: MenuItem ->
val itemId = m.itemId val itemId = m.itemId
if (itemId == R.id.remove_logs) { if (itemId == R.id.remove_logs) {
throw Exception("asffffffffff")
try{ try{
val deleteDialog = MaterialAlertDialogBuilder(requireContext()) val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.confirm_delete_history)) deleteDialog.setTitle(getString(R.string.confirm_delete_history))

View file

@ -80,7 +80,9 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
updateUtil!!.updateApp{ msg -> updateUtil!!.updateApp{ msg ->
lifecycleScope.launch(Dispatchers.Main){ lifecycleScope.launch(Dispatchers.Main){
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show() view?.apply {
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show()
}
} }
} }
} }
@ -119,14 +121,17 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
} }
}.onFailure { }.onFailure {
val msg = it.message ?: requireContext().getString(R.string.errored) val msg = it.message ?: requireContext().getString(R.string.errored)
val snackBar = Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG) view?.apply {
snackBar.setAction(R.string.copy_log){ val snackBar = Snackbar.make(this, msg, Snackbar.LENGTH_LONG)
UiUtil.copyToClipboard(msg, requireActivity()) snackBar.setAction(R.string.copy_log){
UiUtil.copyToClipboard(msg, requireActivity())
}
val snackbarView: View = snackBar.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
snackBar.show()
} }
val snackbarView: View = snackBar.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
snackBar.show()
} }
} }

View file

@ -21,10 +21,14 @@ import com.deniscerri.ytdl.database.models.TerminalItem
import com.deniscerri.ytdl.database.viewmodel.TerminalViewModel import com.deniscerri.ytdl.database.viewmodel.TerminalViewModel
import com.deniscerri.ytdl.ui.adapter.TerminalDownloadsAdapter import com.deniscerri.ytdl.ui.adapter.TerminalDownloadsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.work.DownloadWorker
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnItemClickListener { class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnItemClickListener {
@ -69,30 +73,30 @@ class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnIte
} }
} }
WorkManager.getInstance(requireContext())
.getWorkInfosByTagLiveData("terminal")
.observe(viewLifecycleOwner){ list ->
list.forEach {work ->
if (work == null) return@forEach
val id = work.progress.getInt("id", 0)
if(id == 0) return@forEach
var progress = work.progress.getInt("progress", 0)
if (progress < 0) progress = 0
val output = work.progress.getString("output")
val progressBar = view.findViewWithTag<LinearProgressIndicator>("$id##progress")
val outputText = view.findViewWithTag<TextView>("$id##output")
requireActivity().runOnUiThread {
kotlin.runCatching {
outputText?.text = output
progressBar?.setProgressCompat(progress, true)
}
}
}
}
} }
override fun onStop() {
super.onStop()
EventBus.getDefault().unregister(this)
}
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) {
val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress")
val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output")
requireActivity().runOnUiThread {
kotlin.runCatching {
outputText?.text = event.output
progressBar?.setProgressCompat(event.progress, true)
}
}
}
override fun onCancelClick(itemID: Long) { override fun onCancelClick(itemID: Long) {
terminalViewModel.cancelTerminalDownload(itemID) terminalViewModel.cancelTerminalDownload(itemID)
} }

View file

@ -40,15 +40,20 @@ import com.deniscerri.ytdl.util.Extensions.setCustomTextSize
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.DownloadWorker
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import kotlin.properties.Delegates import kotlin.properties.Delegates

View file

@ -15,11 +15,8 @@ import kotlin.system.exitProcess
class CrashListener(private val context: Context) : Thread.UncaughtExceptionHandler { class CrashListener(private val context: Context) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(p0: Thread, p1: Throwable) { override fun uncaughtException(p0: Thread, p1: Throwable) {
p1.message?.apply { CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
Log.e("ERROR", this) createLog("${p1.message}\n${p1.stackTrace}")
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
createLog(this@apply)
}
} }
} }

View file

@ -467,7 +467,6 @@ class InfoUtil(private val context: Context) {
request.addOption("--skip-download") request.addOption("--skip-download")
request.addOption("-R", "1") request.addOption("-R", "1")
request.addOption("--compat-options", "manifest-filesize-approx") request.addOption("--compat-options", "manifest-filesize-approx")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){ if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4") request.addOption("-4")
@ -993,6 +992,8 @@ class InfoUtil(private val context: Context) {
request.addOption("--no-resize-buffer") request.addOption("--no-resize-buffer")
} }
request.addOption("--socket-timeout", 60)
val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!! val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!!
if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL) if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL)
@ -1059,7 +1060,7 @@ class InfoUtil(private val context: Context) {
} }
if(downloadItem.title.isNotBlank()){ if(downloadItem.title.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.take(150))) request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.take(120)))
} }
@ -1115,7 +1116,7 @@ class InfoUtil(private val context: Context) {
var audioQualityId : String = downloadItem.format.format_id var audioQualityId : String = downloadItem.format.format_id
if (audioQualityId.isBlank() || listOf("0", context.getString(R.string.best_quality), "ba", "best", "").contains(audioQualityId)){ if (audioQualityId.isBlank() || listOf("0", context.getString(R.string.best_quality), "ba", "best", "").contains(audioQualityId)){
audioQualityId = "" audioQualityId = "ba/b"
}else if (listOf(context.getString(R.string.worst_quality), "wa", "worst").contains(audioQualityId)){ }else if (listOf(context.getString(R.string.worst_quality), "wa", "worst").contains(audioQualityId)){
audioQualityId = "wa/w" audioQualityId = "wa/w"
} }
@ -1149,7 +1150,7 @@ class InfoUtil(private val context: Context) {
if(ext.isNotBlank()){ if(ext.isNotBlank()){
if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){ if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){
request.addOption("--audio-format", ext) request.addOption("--audio-format", ext)
formatSorting.append(",ext:$ext") formatSorting.append(",ext::$ext")
} }
} }
@ -1171,7 +1172,7 @@ class InfoUtil(private val context: Context) {
if (downloadItem.playlistTitle.isNotEmpty()) { if (downloadItem.playlistTitle.isNotEmpty()) {
request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s") request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s")
request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(meta_track_number)s") request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(track_number)s")
} else { } else {
request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s") request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
} }
@ -1261,6 +1262,7 @@ class InfoUtil(private val context: Context) {
val f = StringBuilder() val f = StringBuilder()
val preferredCodec = sharedPreferences.getString("video_codec", "") val preferredCodec = sharedPreferences.getString("video_codec", "")
val preferredQuality = sharedPreferences.getString("video_quality", "best")
val vCodecPrefIndex = context.getStringArray(R.array.video_codec_values).indexOf(preferredCodec) val vCodecPrefIndex = context.getStringArray(R.array.video_codec_values).indexOf(preferredCodec)
val vCodecPref = context.getStringArray(R.array.video_codec_values_ytdlp)[vCodecPrefIndex] val vCodecPref = context.getStringArray(R.array.video_codec_values_ytdlp)[vCodecPrefIndex]
@ -1358,12 +1360,18 @@ class InfoUtil(private val context: Context) {
} }
val genericFormats = getGenericVideoFormats(context.resources).map { it.format_id }
StringBuilder().apply { StringBuilder().apply {
if (hasGenericResulutionFormat.isNotBlank()) append(",res:${hasGenericResulutionFormat}") if (hasGenericResulutionFormat.isNotBlank()) {
append(",res:${hasGenericResulutionFormat}")
}else if (genericFormats.contains(videoF) && preferredQuality!!.contains("p_")){
append(",res:${preferredQuality.split("_")[0]}")
}
if (sharedPreferences.getBoolean("prefer_smaller_formats", false)) append(",+size") if (sharedPreferences.getBoolean("prefer_smaller_formats", false)) append(",+size")
if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref") if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref")
if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref") if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref")
if (cont.isNotBlank()) append(",ext:$cont") if (cont.isNotBlank()) append(",ext:$cont:")
if (this.isNotBlank()){ if (this.isNotBlank()){
request.addOption("-S", "+hasaud$this") request.addOption("-S", "+hasaud$this")
} }
@ -1489,7 +1497,7 @@ class InfoUtil(private val context: Context) {
val audioFormatsValues = resources.getStringArray(R.array.audio_formats_values) val audioFormatsValues = resources.getStringArray(R.array.audio_formats_values)
val formats = mutableListOf<Format>() val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("audio_format", "") val containerPreference = sharedPreferences.getString("audio_format", "")
val acodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac")!!.run { val acodecPreference = sharedPreferences.getString("audio_codec", "")!!.run {
if (this.isEmpty()){ if (this.isEmpty()){
resources.getString(R.string.defaultValue) resources.getString(R.string.defaultValue)
}else{ }else{
@ -1508,14 +1516,14 @@ class InfoUtil(private val context: Context) {
val videoFormats = resources.getStringArray(R.array.video_formats_values) val videoFormats = resources.getStringArray(R.array.video_formats_values)
val formats = mutableListOf<Format>() val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("video_format", "") val containerPreference = sharedPreferences.getString("video_format", "")
val audioCodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac")!!.run { val audioCodecPreference = sharedPreferences.getString("audio_codec", "")!!.run {
if (this.isNotEmpty()){ if (this.isNotEmpty()){
val audioCodecs = resources.getStringArray(R.array.audio_codec) val audioCodecs = resources.getStringArray(R.array.audio_codec)
val audioCodecsValues = resources.getStringArray(R.array.audio_codec_values) val audioCodecsValues = resources.getStringArray(R.array.audio_codec_values)
audioCodecs[audioCodecsValues.indexOf(this)] audioCodecs[audioCodecsValues.indexOf(this)]
}else this }else this
} }
val videoCodecPreference = sharedPreferences.getString("video_codec", "avc|h264")!!.run { val videoCodecPreference = sharedPreferences.getString("video_codec", "")!!.run {
if (this.isEmpty()){ if (this.isEmpty()){
resources.getString(R.string.defaultValue) resources.getString(R.string.defaultValue)
}else{ }else{

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdl.util package com.deniscerri.ytdl.util
import android.annotation.SuppressLint
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@ -14,6 +15,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
@ -36,15 +38,11 @@ class NotificationUtil(var context: Context) {
private val erroredDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_ERRORED_CHANNEL_ID) private val erroredDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_ERRORED_CHANNEL_ID)
private val miscDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_MISC_CHANNEL_ID) private val miscDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_MISC_CHANNEL_ID)
private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java) private val notificationManager: NotificationManagerCompat = NotificationManagerCompat.from(context)
private val resources: Resources = context.resources private val resources: Resources = context.resources
fun createNotificationChannel() { fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = context.getSystemService(
NotificationManager::class.java
)
//downloading worker notification //downloading worker notification
var name: CharSequence = resources.getString(R.string.downloading) var name: CharSequence = resources.getString(R.string.downloading)
var description = "WorkManager Default Notification" var description = "WorkManager Default Notification"
@ -176,6 +174,7 @@ class NotificationUtil(var context: Context) {
.build() .build()
} }
@SuppressLint("MissingPermission")
fun createResumeDownload(itemID: Int, title: String?){ fun createResumeDownload(itemID: Int, title: String?){
val notificationBuilder = getBuilder(DOWNLOAD_SERVICE_CHANNEL_ID) val notificationBuilder = getBuilder(DOWNLOAD_SERVICE_CHANNEL_ID)
@ -205,6 +204,7 @@ class NotificationUtil(var context: Context) {
notificationManager.notify(DOWNLOAD_RESUME_NOTIFICATION_ID + itemID, notificationBuilder.build()) notificationManager.notify(DOWNLOAD_RESUME_NOTIFICATION_ID + itemID, notificationBuilder.build())
} }
@SuppressLint("MissingPermission")
fun createUpdatingItemNotification(channel: String){ fun createUpdatingItemNotification(channel: String){
val notificationBuilder = getBuilder(channel) val notificationBuilder = getBuilder(channel)
@ -223,6 +223,7 @@ class NotificationUtil(var context: Context) {
notificationManager.notify(DOWNLOAD_UPDATING_NOTIFICATION_ID, notificationBuilder.build()) notificationManager.notify(DOWNLOAD_UPDATING_NOTIFICATION_ID, notificationBuilder.build())
} }
@SuppressLint("MissingPermission")
fun createDownloadFinished( fun createDownloadFinished(
id: Long, id: Long,
title: String?, title: String?,
@ -319,6 +320,7 @@ class NotificationUtil(var context: Context) {
notificationManager.notify(DOWNLOAD_FINISHED_NOTIFICATION_ID + id.toInt(), notificationBuilder.build()) notificationManager.notify(DOWNLOAD_FINISHED_NOTIFICATION_ID + id.toInt(), notificationBuilder.build())
} }
@SuppressLint("MissingPermission")
fun createDownloadErrored( fun createDownloadErrored(
id: Long, id: Long,
title: String?, title: String?,
@ -379,10 +381,12 @@ class NotificationUtil(var context: Context) {
} }
@SuppressLint("MissingPermission")
fun notify(id: Int, notification: Notification){ fun notify(id: Int, notification: Notification){
notificationManager.notify(id, notification) notificationManager.notify(id, notification)
} }
@SuppressLint("MissingPermission")
fun updateDownloadNotification( fun updateDownloadNotification(
id: Int, id: Int,
desc: String, desc: String,
@ -428,6 +432,8 @@ class NotificationUtil(var context: Context) {
e.printStackTrace() e.printStackTrace()
} }
} }
@SuppressLint("MissingPermission")
fun updateTerminalDownloadNotification( fun updateTerminalDownloadNotification(
id: Int, id: Int,
desc: String, desc: String,
@ -488,6 +494,8 @@ class NotificationUtil(var context: Context) {
.build() .build()
} }
@SuppressLint("MissingPermission")
fun updateCacheMovingNotification(id: Int, progress: Int, totalFiles: Int) { fun updateCacheMovingNotification(id: Int, progress: Int, totalFiles: Int) {
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
val contentText = "${progress}/${totalFiles}" val contentText = "${progress}/${totalFiles}"
@ -542,6 +550,7 @@ class NotificationUtil(var context: Context) {
.build() .build()
} }
@SuppressLint("MissingPermission")
fun updateFormatUpdateNotification( fun updateFormatUpdateNotification(
workID: Int, workID: Int,
workTag: String, workTag: String,
@ -576,6 +585,7 @@ class NotificationUtil(var context: Context) {
} }
@SuppressLint("MissingPermission")
fun showFormatsUpdatedNotification(downloadIds: List<Long>) { fun showFormatsUpdatedNotification(downloadIds: List<Long>) {
val notificationBuilder = getBuilder(DOWNLOAD_FINISHED_CHANNEL_ID) val notificationBuilder = getBuilder(DOWNLOAD_FINISHED_CHANNEL_ID)
@ -606,6 +616,8 @@ class NotificationUtil(var context: Context) {
notificationManager.notify(FORMAT_UPDATING_FINISHED_NOTIFICATION_ID, notificationBuilder.build()) notificationManager.notify(FORMAT_UPDATING_FINISHED_NOTIFICATION_ID, notificationBuilder.build())
} }
@SuppressLint("MissingPermission")
fun showQueriesFinished() { fun showQueriesFinished() {
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)

View file

@ -100,6 +100,7 @@ import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
import java.util.Queue
object UiUtil { object UiUtil {
@ -709,7 +710,13 @@ object UiUtil {
} }
} }
DownloadRepository.Status.Queued -> { DownloadRepository.Status.Queued -> {
download!!.visibility = View.GONE download!!.text = context.getString(R.string.configure_download)
download.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_retries, 0, 0, 0);
download.setOnClickListener {
longClickDownloadButton(item)
bottomSheet.cancel()
true
}
} }
DownloadRepository.Status.Scheduled -> { DownloadRepository.Status.Scheduled -> {
download!!.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_downloads, 0, 0, 0); download!!.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_downloads, 0, 0, 0);
@ -724,9 +731,11 @@ object UiUtil {
} }
} }
download?.setOnClickListener { if (status != DownloadRepository.Status.Queued){
bottomSheet.dismiss() download?.setOnClickListener {
downloadItem(item) bottomSheet.dismiss()
downloadItem(item)
}
} }
bottomSheet.show() bottomSheet.show()
@ -860,9 +869,10 @@ object UiUtil {
showGeneratedCommand(context, item.command) showGeneratedCommand(context, item.command)
} }
location?.isVisible = true val availableFiles = item.downloadPath.filter { FileUtil.exists(it) }
location?.isVisible = availableFiles.isNotEmpty()
location?.setOnClickListener { location?.setOnClickListener {
showFullTextDialog(context, item.downloadPath.joinToString("\n"), context.getString(R.string.location)) showFullTextDialog(context, availableFiles.joinToString("\n"), context.getString(R.string.location))
} }
@ -1316,7 +1326,8 @@ object UiUtil {
val cut = view.findViewById<Chip>(R.id.cut) val cut = view.findViewById<Chip>(R.id.cut)
if (items.size > 1 || items.first().url.isEmpty()) cut.isVisible = false if (items.size > 1 || items.first().url.isEmpty()) cut.isVisible = false
else{ else{
if(items[0].duration.isNotEmpty()){ val invalidDuration = items[0].duration == "-1"
if(items[0].duration.isNotEmpty() && !invalidDuration){
val downloadItem = items[0] val downloadItem = items[0]
cut.alpha = 1f cut.alpha = 1f
if (downloadItem.downloadSections.isNotBlank()) cut.text = downloadItem.downloadSections if (downloadItem.downloadSections.isNotBlank()) cut.text = downloadItem.downloadSections
@ -1355,8 +1366,10 @@ object UiUtil {
} }
}else{ }else{
cut.alpha = 0.3f cut.alpha = 0.3f
cut.setOnClickListener { if (!invalidDuration) {
cutDisabledClicked() cut.setOnClickListener {
cutDisabledClicked()
}
} }
} }
} }
@ -1549,7 +1562,8 @@ object UiUtil {
if (items.size > 1 || items.first().url.isEmpty()) cut.isVisible = false if (items.size > 1 || items.first().url.isEmpty()) cut.isVisible = false
else{ else{
val downloadItem = items[0] val downloadItem = items[0]
if (downloadItem.duration.isNotEmpty()){ val invalidDuration = downloadItem.duration == "-1"
if (downloadItem.duration.isNotEmpty() && !invalidDuration){
cut.alpha = 1f cut.alpha = 1f
if (downloadItem.downloadSections.isNotBlank()) cut.text = downloadItem.downloadSections if (downloadItem.downloadSections.isNotBlank()) cut.text = downloadItem.downloadSections
val cutVideoListener = object : VideoCutListener { val cutVideoListener = object : VideoCutListener {
@ -1579,8 +1593,10 @@ object UiUtil {
}else{ }else{
cut.alpha = 0.3f cut.alpha = 0.3f
cut.setOnClickListener { if (!invalidDuration) {
cutDisabledClicked() cut.setOnClickListener {
cutDisabledClicked()
}
} }
} }
} }

View file

@ -39,6 +39,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
import kotlin.random.Random import kotlin.random.Random
@ -88,6 +89,7 @@ class DownloadWorker(
setForegroundAsync(foregroundInfo) setForegroundAsync(foregroundInfo)
queuedItems.collect { items -> queuedItems.collect { items ->
if (isStopped) return@collect
runningYTDLInstances.clear() runningYTDLInstances.clear()
val activeDownloads = dao.getActiveDownloadsList() val activeDownloads = dao.getActiveDownloadsList()
activeDownloads.forEach { activeDownloads.forEach {
@ -120,7 +122,7 @@ class DownloadWorker(
//update item if its incomplete //update item if its incomplete
resultRepo.updateDownloadItem(downloadItem)?.apply { resultRepo.updateDownloadItem(downloadItem)?.apply {
val status = dao.checkStatus(this.id) val status = dao.checkStatus(this.id)
if (listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused).contains(status)){ if (status == DownloadRepository.Status.Active){
dao.updateWithoutUpsert(this) dao.updateWithoutUpsert(this)
} }
} }
@ -158,9 +160,11 @@ class DownloadWorker(
dao.update(downloadItem) dao.update(downloadItem)
} }
val eventBus = EventBus.getDefault()
runCatching { runCatching {
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line ->
setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line.chunked(5000).ifEmpty { listOf("") }.first().toString(), "id" to downloadItem.id)) eventBus.post(WorkerProgress(progress.toInt(), line, downloadItem.id))
val title: String = downloadItem.title.ifEmpty { downloadItem.url } val title: String = downloadItem.title.ifEmpty { downloadItem.url }
notificationUtil.updateDownloadNotification( notificationUtil.updateDownloadNotification(
downloadItem.id.toInt(), downloadItem.id.toInt(),
@ -181,7 +185,7 @@ class DownloadWorker(
var finalPaths : MutableList<String>? var finalPaths : MutableList<String>?
if (noCache){ if (noCache){
setProgressAsync(workDataOf("progress" to 100, "output" to "Scanning Files", "id" to downloadItem.id)) eventBus.post(WorkerProgress(100, "Scanning Files", downloadItem.id))
val outputSequence = it.out.split("\n") val outputSequence = it.out.split("\n")
finalPaths = finalPaths =
outputSequence.asSequence() outputSequence.asSequence()
@ -206,16 +210,16 @@ class DownloadWorker(
} }
}else{ }else{
//move file from internal to set download directory //move file from internal to set download directory
setProgressAsync(workDataOf("progress" to 100, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id)) eventBus.post(WorkerProgress(100, "Moving file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id))
try { try {
finalPaths = withContext(Dispatchers.IO){ finalPaths = withContext(Dispatchers.IO){
FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p -> FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p ->
setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id)) eventBus.post(WorkerProgress(p, "Moving file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id))
} }
}.filter { !it.matches("\\.(description)|(txt)\$".toRegex()) }.toMutableList() }.filter { !it.matches("\\.(description)|(txt)\$".toRegex()) }.toMutableList()
if (finalPaths.isNotEmpty()){ if (finalPaths.isNotEmpty()){
setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id)) eventBus.post(WorkerProgress(100, "Moved file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id))
}else{ }else{
finalPaths = mutableListOf(context.getString(R.string.unfound_file)) finalPaths = mutableListOf(context.getString(R.string.unfound_file))
} }
@ -285,7 +289,7 @@ class DownloadWorker(
if (wasQuickDownloaded && createResultItem){ if (wasQuickDownloaded && createResultItem){
runCatching { runCatching {
setProgressAsync(workDataOf("progress" to 100, "output" to "Creating Result Items", "id" to downloadItem.id)) eventBus.post(WorkerProgress(100, "Creating Result Items", downloadItem.id))
runBlocking { runBlocking {
infoUtil.getFromYTDL(downloadItem.url).forEach { res -> infoUtil.getFromYTDL(downloadItem.url).forEach { res ->
if (res != null) { if (res != null) {
@ -304,50 +308,46 @@ class DownloadWorker(
} }
}.onFailure { }.onFailure {
if (this@DownloadWorker.isStopped) return@onFailure
FileUtil.deleteConfigFiles(request) FileUtil.deleteConfigFiles(request)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
} }
if (it is YoutubeDL.CanceledException) { if (this@DownloadWorker.isStopped) return@onFailure
if (it is YoutubeDL.CanceledException) return@onFailure
}else{ if(it.message != null){
if(it.message != null){ if (logDownloads){
if (logDownloads){ logRepo.update(it.message!!, logItem.id)
logRepo.update(it.message!!, logItem.id) }else{
}else{ logString.append("${it.message}\n")
logString.append("${it.message}\n") logItem.content = logString.toString()
logItem.content = logString.toString() val logID = logRepo.insert(logItem)
val logID = logRepo.insert(logItem) downloadItem.logID = logID
downloadItem.logID = logID
}
} }
tempFileDir.delete()
handler.postDelayed({
Toast.makeText(context, it.message, Toast.LENGTH_LONG).show()
}, 1000)
Log.e(TAG, context.getString(R.string.failed_download), it)
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
downloadItem.status = DownloadRepository.Status.Error.toString()
runBlocking {
dao.update(downloadItem)
}
notificationUtil.createDownloadErrored(
downloadItem.id,
downloadItem.title.ifEmpty { downloadItem.url },
it.message,
downloadItem.logID,
resources
)
setProgressAsync(workDataOf("progress" to 100, "output" to it.toString(), "id" to downloadItem.id))
} }
tempFileDir.delete()
handler.postDelayed({
Toast.makeText(context, it.message, Toast.LENGTH_LONG).show()
}, 1000)
Log.e(TAG, context.getString(R.string.failed_download), it)
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
downloadItem.status = DownloadRepository.Status.Error.toString()
runBlocking {
dao.update(downloadItem)
}
notificationUtil.createDownloadErrored(
downloadItem.id,
downloadItem.title.ifEmpty { downloadItem.url },
it.message,
downloadItem.logID,
resources
)
eventBus.post(WorkerProgress(100, it.toString(), downloadItem.id))
} }
} }
} }
@ -369,4 +369,10 @@ class DownloadWorker(
const val TAG = "DownloadWorker" const val TAG = "DownloadWorker"
} }
class WorkerProgress(
val progress: Int,
val output: String,
val downloadItemID: Long
)
} }

View file

@ -118,7 +118,7 @@ class ObserveSourceWorker(
} }
items.forEach { items.forEach {
if (it.status != DownloadRepository.Status.ActivePaused.toString()) it.status = DownloadRepository.Status.Queued.toString() it.status = DownloadRepository.Status.Queued.toString()
val currentCommand = infoUtil.buildYoutubeDLRequest(it) val currentCommand = infoUtil.buildYoutubeDLRequest(it)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand) val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
val existingDownload = activeAndQueuedDownloads.firstOrNull{d -> val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->

View file

@ -11,7 +11,6 @@ import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.Format
@ -28,7 +27,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus
import java.io.File import java.io.File
@ -99,6 +98,8 @@ class TerminalDownloadWorker(
System.currentTimeMillis(), System.currentTimeMillis(),
) )
val eventBus = EventBus.getDefault()
kotlin.runCatching { kotlin.runCatching {
if (logDownloads){ if (logDownloads){
runBlocking { runBlocking {
@ -108,10 +109,7 @@ class TerminalDownloadWorker(
YoutubeDL.getInstance().execute(request, itemId.toString()){ progress, _, line -> YoutubeDL.getInstance().execute(request, itemId.toString()){ progress, _, line ->
runBlocking { runBlocking {
line.chunked(10000).forEach { eventBus.post(DownloadWorker.WorkerProgress(progress.toInt(), line, itemId.toLong()))
setProgress(workDataOf("progress" to progress.toInt(), "output" to it, "id" to itemId, "log" to logDownloads))
Thread.sleep(100)
}
} }
val title: String = command.take(65) val title: String = command.take(65)
@ -131,7 +129,7 @@ class TerminalDownloadWorker(
//move file from internal to set download directory //move file from internal to set download directory
try { try {
FileUtil.moveFile(File(FileUtil.getCachePath(context) + "/TERMINAL/" + itemId),context, downloadLocation!!, false){ p -> FileUtil.moveFile(File(FileUtil.getCachePath(context) + "/TERMINAL/" + itemId),context, downloadLocation!!, false){ p ->
setProgressAsync(workDataOf("progress" to p)) eventBus.post(DownloadWorker.WorkerProgress(p, "", itemId.toLong()))
} }
}catch (e: Exception){ }catch (e: Exception){
e.printStackTrace() e.printStackTrace()

View file

@ -105,42 +105,72 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:title="@string/app_name" /> app:title="@string/app_name" />
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="0dp"
android:translationZ="250dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/search_bar"
app:layout_constraintEnd_toEndOf="@+id/search_bar"
app:layout_constraintStart_toStartOf="@+id/search_bar"
app:layout_constraintTop_toTopOf="@+id/search_bar"
app:shimmer_auto_start="true">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="30dp">
</com.google.android.material.card.MaterialCardView>
</com.facebook.shimmer.ShimmerFrameLayout>
<com.google.android.material.search.SearchBar <com.google.android.material.search.SearchBar
android:id="@+id/search_bar" android:id="@+id/search_bar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center_horizontal"
app:layout_scrollFlags="scroll|enterAlways"
android:hint="@string/search_hint" android:hint="@string/search_hint"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0" app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/home_toolbar" app:layout_constraintStart_toEndOf="@+id/home_toolbar"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/main_menu" /> app:menu="@menu/main_menu">
</com.google.android.material.search.SearchBar>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
<RelativeLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent" android:layout_width="match_parent"
android:id="@+id/recyclerViewHome" android:layout_height="match_parent">
android:orientation="vertical"
android:layout_height="wrap_content" <androidx.recyclerview.widget.RecyclerView
android:scrollbars="none" android:layout_width="match_parent"
android:clipToPadding="false" android:id="@+id/recyclerViewHome"
android:paddingBottom="100dp" android:orientation="vertical"
app:spanCount="2" android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" android:scrollbars="none"
/> android:clipToPadding="false"
<com.facebook.shimmer.ShimmerFrameLayout android:paddingBottom="100dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:spanCount="2"
android:layout_width="match_parent" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
android:layout_height="match_parent" />
android:id="@+id/shimmer_results_framelayout"
android:orientation="vertical"> <com.facebook.shimmer.ShimmerFrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/recyclerViewHome"
android:id="@+id/shimmer_results_framelayout"
android:orientation="vertical">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -222,6 +252,10 @@
</com.facebook.shimmer.ShimmerFrameLayout> </com.facebook.shimmer.ShimmerFrameLayout>
</RelativeLayout>
<LinearLayout <LinearLayout
android:id="@+id/home_fabs" android:id="@+id/home_fabs"
@ -238,6 +272,7 @@
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:text="@string/link_you_copied" android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginBottom="10dp"
android:contentDescription="@string/link_you_copied" android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard" app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" /> app:useCompatPadding="true" />

View file

@ -91,148 +91,184 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.google.android.material.search.SearchBar <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/search_bar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_gravity="end" android:layout_gravity="end"
app:layout_scrollFlags="scroll|enterAlways" android:layout_height="wrap_content">
android:hint="@string/search_hint"
app:layout_constraintBottom_toBottomOf="parent" <com.facebook.shimmer.ShimmerFrameLayout
app:layout_constraintEnd_toEndOf="parent" android:id="@+id/progress"
app:layout_constraintHorizontal_bias="1.0" android:layout_width="0dp"
app:layout_constraintStart_toEndOf="@+id/home_toolbar" android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent" android:translationZ="250dp"
app:menu="@menu/main_menu" /> android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/search_bar"
app:layout_constraintEnd_toEndOf="@+id/search_bar"
app:layout_constraintStart_toStartOf="@+id/search_bar"
app:layout_constraintTop_toTopOf="@+id/search_bar"
app:shimmer_auto_start="true">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="30dp">
</com.google.android.material.card.MaterialCardView>
</com.facebook.shimmer.ShimmerFrameLayout>
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:hint="@string/search_hint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/main_menu">
</com.google.android.material.search.SearchBar>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<RelativeLayout
<androidx.recyclerview.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent" android:layout_width="match_parent"
android:id="@+id/recyclerViewHome" android:layout_height="match_parent">
android:orientation="vertical"
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
app:spanCount="3"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/>
<com.facebook.shimmer.ShimmerFrameLayout <androidx.recyclerview.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/shimmer_results_framelayout"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="10dp" android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
app:spanCount="3"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/>
<com.facebook.shimmer.ShimmerFrameLayout
android:layout_width="match_parent"
android:layout_below="@id/recyclerViewHome"
android:layout_height="match_parent"
android:id="@+id/shimmer_results_framelayout"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:weightSum="3" android:layout_height="wrap_content"
android:layout_height="200dp"> android:layout_margin="10dp"
android:orientation="vertical">
<include <LinearLayout
android:layout_weight="1" android:layout_width="match_parent"
layout="@layout/result_card_shimmer" android:weightSum="3"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout> <include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<LinearLayout </LinearLayout>
android:layout_width="match_parent"
android:weightSum="3"
android:layout_height="200dp">
<include <LinearLayout
android:layout_weight="1" android:layout_width="match_parent"
layout="@layout/result_card_shimmer" android:weightSum="3"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout> <include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<LinearLayout </LinearLayout>
android:layout_width="match_parent"
android:weightSum="3"
android:layout_height="200dp">
<include <LinearLayout
android:layout_weight="1" android:layout_width="match_parent"
layout="@layout/result_card_shimmer" android:weightSum="3"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout> <include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<LinearLayout </LinearLayout>
android:layout_width="match_parent"
android:weightSum="3" <LinearLayout
android:layout_height="200dp"> android:layout_width="match_parent"
android:weightSum="3"
android:layout_height="wrap_content">
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout>
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </com.facebook.shimmer.ShimmerFrameLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
</RelativeLayout>
<LinearLayout <LinearLayout
android:id="@+id/home_fabs" android:id="@+id/home_fabs"
@ -249,6 +285,7 @@
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:text="@string/link_you_copied" android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginBottom="10dp"
android:contentDescription="@string/link_you_copied" android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard" app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" /> app:useCompatPadding="true" />

View file

@ -105,123 +105,153 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:title="@string/app_name" /> app:title="@string/app_name" />
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="0dp"
android:translationZ="250dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/search_bar"
app:layout_constraintEnd_toEndOf="@+id/search_bar"
app:layout_constraintStart_toStartOf="@+id/search_bar"
app:layout_constraintTop_toTopOf="@+id/search_bar"
app:shimmer_auto_start="true">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="30dp">
</com.google.android.material.card.MaterialCardView>
</com.facebook.shimmer.ShimmerFrameLayout>
<com.google.android.material.search.SearchBar <com.google.android.material.search.SearchBar
android:id="@+id/search_bar" android:id="@+id/search_bar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center_horizontal"
app:layout_scrollFlags="scroll|enterAlways"
android:hint="@string/search_hint" android:hint="@string/search_hint"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0" app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/home_toolbar" app:layout_constraintStart_toEndOf="@+id/home_toolbar"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/main_menu" /> app:menu="@menu/main_menu">
</com.google.android.material.search.SearchBar>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView <RelativeLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent" android:layout_width="match_parent"
android:id="@+id/recyclerViewHome" android:layout_height="match_parent">
android:orientation="vertical"
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
app:spanCount="2"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/>
<com.facebook.shimmer.ShimmerFrameLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/shimmer_results_framelayout"
android:orientation="vertical">
<LinearLayout <androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent" android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="10dp" android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
app:spanCount="2"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/>
<com.facebook.shimmer.ShimmerFrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/recyclerViewHome"
android:id="@+id/shimmer_results_framelayout"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:weightSum="2" android:layout_height="wrap_content"
android:layout_height="250dp"> android:layout_margin="10dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:weightSum="2"
android:layout_height="wrap_content">
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:weightSum="2"
android:layout_height="wrap_content">
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:weightSum="2"
android:layout_height="wrap_content">
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout>
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout> </LinearLayout>
<LinearLayout </com.facebook.shimmer.ShimmerFrameLayout>
android:layout_width="match_parent"
android:weightSum="2"
android:layout_height="250dp">
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:weightSum="2"
android:layout_height="250dp">
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout>
</LinearLayout> </RelativeLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<LinearLayout <LinearLayout
android:id="@+id/home_fabs" android:id="@+id/home_fabs"
@ -237,6 +267,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:text="@string/link_you_copied" android:text="@string/link_you_copied"
android:layout_marginBottom="10dp"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied" android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard" app:icon="@drawable/ic_clipboard"

View file

@ -91,167 +91,203 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.google.android.material.search.SearchBar <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/search_bar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_gravity="end" android:layout_gravity="end"
app:layout_scrollFlags="scroll|enterAlways" android:layout_height="wrap_content">
android:hint="@string/search_hint"
app:layout_constraintBottom_toBottomOf="parent" <com.facebook.shimmer.ShimmerFrameLayout
app:layout_constraintEnd_toEndOf="parent" android:id="@+id/progress"
app:layout_constraintHorizontal_bias="1.0" android:layout_width="0dp"
app:layout_constraintStart_toEndOf="@+id/home_toolbar" android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent" android:translationZ="250dp"
app:menu="@menu/main_menu" /> android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/search_bar"
app:layout_constraintEnd_toEndOf="@+id/search_bar"
app:layout_constraintStart_toStartOf="@+id/search_bar"
app:layout_constraintTop_toTopOf="@+id/search_bar"
app:shimmer_auto_start="true">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="30dp">
</com.google.android.material.card.MaterialCardView>
</com.facebook.shimmer.ShimmerFrameLayout>
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:hint="@string/search_hint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/main_menu">
</com.google.android.material.search.SearchBar>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView <RelativeLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent" android:layout_width="match_parent"
android:id="@+id/recyclerViewHome" android:layout_height="match_parent">
android:orientation="vertical"
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
app:spanCount="4"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/>
<com.facebook.shimmer.ShimmerFrameLayout <androidx.recyclerview.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/shimmer_results_framelayout"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="10dp" android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
app:spanCount="4"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/>
<com.facebook.shimmer.ShimmerFrameLayout
android:layout_width="match_parent"
android:layout_below="@id/recyclerViewHome"
android:layout_height="match_parent"
android:id="@+id/shimmer_results_framelayout"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:weightSum="4" android:layout_height="wrap_content"
android:layout_height="200dp" android:layout_margin="10dp"
android:baselineAligned="false"> android:orientation="vertical">
<include <LinearLayout
android:layout_weight="1" android:layout_width="match_parent"
layout="@layout/result_card_shimmer" android:weightSum="4"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="0dp" /> android:baselineAligned="false">
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout> <include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<LinearLayout </LinearLayout>
android:layout_width="match_parent"
android:weightSum="4"
android:layout_height="200dp">
<include <LinearLayout
android:layout_weight="1" android:layout_width="match_parent"
layout="@layout/result_card_shimmer" android:weightSum="4"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout> <include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<LinearLayout </LinearLayout>
android:layout_width="match_parent"
android:weightSum="4"
android:layout_height="200dp">
<include <LinearLayout
android:layout_weight="1" android:layout_width="match_parent"
layout="@layout/result_card_shimmer" android:weightSum="4"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout> <include
<LinearLayout android:layout_weight="1"
android:layout_width="match_parent" layout="@layout/result_card_shimmer"
android:weightSum="4" android:layout_height="wrap_content"
android:layout_height="200dp"> android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:weightSum="4"
android:layout_height="wrap_content">
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout>
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
android:layout_height="wrap_content"
android:layout_width="0dp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </com.facebook.shimmer.ShimmerFrameLayout>
</com.facebook.shimmer.ShimmerFrameLayout> </RelativeLayout>
<LinearLayout <LinearLayout
android:id="@+id/home_fabs" android:id="@+id/home_fabs"
@ -268,6 +304,7 @@
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:text="@string/link_you_copied" android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginBottom="10dp"
android:contentDescription="@string/link_you_copied" android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard" app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" /> app:useCompatPadding="true" />

View file

@ -166,6 +166,23 @@
app:layout_constraintVertical_bias="0.51" app:layout_constraintVertical_bias="0.51"
> >
<com.google.android.material.button.MaterialButton
android:id="@+id/active_download_resume"
style="?attr/materialIconButtonFilledStyle"
android:visibility="gone"
android:layout_width="wrap_content"
android:contentDescription="@string/resume"
android:layout_height="wrap_content"
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_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearlayout2_horizontalscrollview"
app:layout_constraintVertical_bias="0.51" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/active_download_delete" android:id="@+id/active_download_delete"
style="?attr/materialIconButtonFilledStyle" style="?attr/materialIconButtonFilledStyle"

View file

@ -181,8 +181,7 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/from_textinput_edittext" android:id="@+id/from_textinput_edittext"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@ -209,8 +208,7 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/to_textinput_edittext" android:id="@+id/to_textinput_edittext"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>

View file

@ -41,6 +41,29 @@
android:orientation="horizontal" android:orientation="horizontal"
android:paddingTop="20dp"> android:paddingTop="20dp">
<com.facebook.shimmer.ShimmerFrameLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginEnd="20dp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/shimmer_loading_title"
android:orientation="vertical">
<TextView
android:id="@+id/bottom_sheet_loading_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:singleLine="false"
android:text="@string/loading"
android:textSize="25sp" />
</com.facebook.shimmer.ShimmerFrameLayout>
<TextView <TextView
android:id="@+id/bottom_sheet_title" android:id="@+id/bottom_sheet_title"
android:layout_width="0dp" android:layout_width="0dp"
@ -53,6 +76,28 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<com.facebook.shimmer.ShimmerFrameLayout
android:layout_width="0dp"
android:visibility="gone"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_bias="0.0"
android:layout_marginEnd="20dp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/shimmer_loading_title"
android:id="@+id/shimmer_loading_subtitle"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:singleLine="false"
android:text="@string/please_wait"
android:textSize="12sp" />
</com.facebook.shimmer.ShimmerFrameLayout>
<TextView <TextView
android:id="@+id/bottom_sheet_subtitle" android:id="@+id/bottom_sheet_subtitle"
@ -114,6 +159,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" android:visibility="gone"
android:paddingEnd="20dp" android:paddingEnd="20dp"
android:paddingStart="0dp"
android:paddingTop="10dp" android:paddingTop="10dp"
android:text="@string/file_size" android:text="@string/file_size"
android:textStyle="bold" android:textStyle="bold"
@ -124,12 +170,6 @@
</LinearLayout> </LinearLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loadingItemsProgress"
android:layout_width="match_parent"
android:indeterminate="true"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloadMultipleRecyclerview" android:id="@+id/downloadMultipleRecyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
@ -141,13 +141,45 @@
app:layout_scrollFlags="scroll|enterAlways" app:layout_scrollFlags="scroll|enterAlways"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<com.google.android.material.search.SearchBar <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/search_bar"
app:menu="@menu/main_menu"
app:layout_scrollFlags="scroll|enterAlways"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_marginVertical="15dp"
android:hint="@string/search_hint" /> app:layout_scrollFlags="scroll|enterAlways"
android:layout_height="wrap_content">
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="0dp"
android:translationZ="250dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/search_bar"
app:layout_constraintEnd_toEndOf="@+id/search_bar"
app:layout_constraintStart_toStartOf="@+id/search_bar"
app:layout_constraintTop_toTopOf="@+id/search_bar"
app:shimmer_auto_start="true">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="30dp">
</com.google.android.material.card.MaterialCardView>
</com.facebook.shimmer.ShimmerFrameLayout>
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/search_hint"
app:menu="@menu/main_menu" >
</com.google.android.material.search.SearchBar>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
@ -167,6 +199,7 @@
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:text="@string/link_you_copied" android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginBottom="10dp"
android:contentDescription="@string/link_you_copied" android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard" app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" /> app:useCompatPadding="true" />

View file

@ -1,112 +1,118 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout android:layout_width="match_parent" <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent"
android:layout_height="230dp" android:layout_height="match_parent"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.cardview.widget.CardView <RelativeLayout android:layout_width="0dp"
android:layout_width="match_parent" android:layout_height="0dp"
android:layout_height="match_parent" app:layout_constraintDimensionRatio="H,16:9"
app:cardCornerRadius="20dp" app:layout_constraintStart_toStartOf="parent"
app:cardElevation="10dp" app:layout_constraintEnd_toEndOf="parent"
app:cardMaxElevation="12dp" app:layout_constraintTop_toTopOf="parent"
app:cardBackgroundColor="@color/grey" xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardPreventCornerOverlap="true"
android:layout_margin="10dp"
> >
<androidx.appcompat.widget.AppCompatImageView <androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:layout_width="300dp"
android:layout_height="10dp"
android:layout_marginTop="20dp"
android:layout_marginLeft="10dp"
android:paddingRight="20dp"
android:paddingBottom="20dp"
android:paddingLeft="20dp"
android:textSize="15sp"
android:textColor="@color/white"
android:background="@color/light_grey"
android:textStyle="bold"
android:shadowRadius="2"
android:shadowDx="4"
android:shadowDy="4"
android:shadowColor="@color/black" />
<TextView
android:layout_width="200dp"
android:layout_height="10dp"
android:layout_marginTop="40dp"
android:layout_marginLeft="10dp"
android:paddingRight="20dp"
android:paddingBottom="20dp"
android:paddingLeft="20dp"
android:textSize="15sp"
android:textColor="@color/white"
android:background="@color/light_grey"
android:textStyle="bold"
android:shadowRadius="2"
android:shadowDx="4"
android:shadowDy="4"
android:shadowColor="@color/black" />
<TextView
android:layout_width="50dp"
android:layout_height="10dp"
android:layout_gravity="bottom"
android:layout_marginBottom="20dp"
android:layout_marginLeft="10dp"
android:background="@color/light_grey"
android:gravity="bottom"
android:paddingTop="10dp"
android:paddingRight="20dp"
android:paddingBottom="20dp"
android:paddingLeft="20dp"
android:textSize="15sp"
android:textColor="@color/white"
android:textStyle="bold"
android:shadowRadius="1.5"
android:shadowDx="4"
android:shadowDy="4"
android:shadowColor="@color/black" />
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="bottom|right" app:cardCornerRadius="20dp"
android:orientation="horizontal" app:cardElevation="10dp"
android:paddingBottom="16dp" app:cardMaxElevation="12dp"
android:paddingLeft="10dp" app:cardBackgroundColor="@color/grey"
android:paddingRight="10dp"> app:cardPreventCornerOverlap="true"
android:layout_margin="10dp"
>
<com.google.android.material.button.MaterialButton <androidx.appcompat.widget.AppCompatImageView
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon" android:layout_width="match_parent"
android:id="@+id/download_music" android:layout_height="match_parent" />
android:layout_width="48dp"
android:layout_height="48dp" <TextView
app:cornerRadius="10dp" android:layout_width="300dp"
app:backgroundTint="@color/light_grey" android:layout_height="10dp"
android:layout_marginTop="20dp"
android:layout_marginLeft="10dp" android:layout_marginLeft="10dp"
/> android:paddingRight="20dp"
android:paddingBottom="20dp"
android:paddingLeft="20dp"
android:textSize="15sp"
android:textColor="@color/white"
android:background="@color/light_grey"
android:textStyle="bold"
android:shadowRadius="2"
android:shadowDx="4"
android:shadowDy="4"
android:shadowColor="@color/black" />
<com.google.android.material.button.MaterialButton <TextView
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon" android:layout_width="200dp"
android:id="@+id/download_video" android:layout_height="10dp"
android:layout_width="48dp" android:layout_marginTop="40dp"
android:layout_height="48dp"
app:cornerRadius="10dp"
app:backgroundTint="@color/light_grey"
android:layout_marginLeft="10dp" android:layout_marginLeft="10dp"
/> android:paddingRight="20dp"
android:paddingBottom="20dp"
android:paddingLeft="20dp"
android:textSize="15sp"
android:textColor="@color/white"
android:background="@color/light_grey"
android:textStyle="bold"
android:shadowRadius="2"
android:shadowDx="4"
android:shadowDy="4"
android:shadowColor="@color/black" />
</LinearLayout> <TextView
android:layout_width="50dp"
android:layout_height="10dp"
android:layout_gravity="bottom"
android:layout_marginBottom="20dp"
android:layout_marginLeft="10dp"
android:background="@color/light_grey"
android:gravity="bottom"
android:paddingTop="10dp"
android:paddingRight="20dp"
android:paddingBottom="20dp"
android:paddingLeft="20dp"
android:textSize="15sp"
android:textColor="@color/white"
android:textStyle="bold"
android:shadowRadius="1.5"
android:shadowDx="4"
android:shadowDy="4"
android:shadowColor="@color/black" />
</androidx.cardview.widget.CardView> <LinearLayout
</RelativeLayout> android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom|right"
android:orientation="horizontal"
android:paddingBottom="16dp"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon"
android:id="@+id/download_music"
android:layout_width="48dp"
android:layout_height="48dp"
app:cornerRadius="10dp"
app:backgroundTint="@color/light_grey"
android:layout_marginLeft="10dp"
/>
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon"
android:id="@+id/download_video"
android:layout_width="48dp"
android:layout_height="48dp"
app:cornerRadius="10dp"
app:backgroundTint="@color/light_grey"
android:layout_marginLeft="10dp"
/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -22,38 +22,43 @@
app:menu="@menu/select_playlist_menu" app:menu="@menu/select_playlist_menu"
android:layout_gravity="bottom"> android:layout_gravity="bottom">
<com.google.android.material.textfield.TextInputLayout <LinearLayout
android:id="@+id/from_textinput" android:layout_width="wrap_content"
style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:layout_marginStart="50dp" android:layout_marginStart="50dp"
android:layout_marginEnd="10dp" android:layout_height="wrap_content">
android:hint="@string/start">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent" android:id="@+id/from_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
android:layout_width="70dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="number" /> android:layout_marginEnd="10dp"
android:hint="@string/start">
</com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
<com.google.android.material.textfield.TextInputLayout </com.google.android.material.textfield.TextInputLayout>
android:id="@+id/to_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:hint="@string/end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/colon">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent" android:id="@+id/to_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
android:layout_width="70dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="text" /> android:hint="@string/end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/colon">
</com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.bottomappbar.BottomAppBar> </com.google.android.material.bottomappbar.BottomAppBar>

View file

@ -378,4 +378,24 @@
<string name="hour">Ora</string> <string name="hour">Ora</string>
<string name="no_download_fragments">Mos shkarkoni si Fragmente</string> <string name="no_download_fragments">Mos shkarkoni si Fragmente</string>
<string name="no_download_fragments_summary">Mos përdorni skedarë .part dhe shkruani direkt në skedarin dalës</string> <string name="no_download_fragments_summary">Mos përdorni skedarë .part dhe shkruani direkt në skedarin dalës</string>
<string name="subtitles">Titrat</string>
<string name="prevent_duplicate_downloads">Parandalo Shkarkimet Duplikate</string>
<string name="prevent_duplicate_url_type">URL dhe Tipi i Shkarkimit</string>
<string name="prevent_duplicate_config">Konfigurimi i plotë</string>
<string name="copy_url">Kopjo URL</string>
<string name="move_top">Vendose në fillim</string>
<string name="move_bottom">Vendose në fund</string>
<string name="scheduled">Të Planifikuar</string>
<string name="clear_scheduled">Fshi të planifikuarit</string>
<string name="minute">Minuta</string>
<string name="second">Sekonda</string>
<string name="milliseconds">Milisekonda</string>
<string name="format_importance_audio">Rradha e rëndësisë së Formateve (Audio)</string>
<string name="format_importance_video">Rradha e rëndësisë së Formateve (Video)</string>
<string name="format_importance_note">Rregullo rëndësinë e elementit të formatit. Kjo porosi do të përdoret vetëm kur aplikacioni merr formatet në kartën e shkarkimit dhe zgjedh automatikisht formatin!</string>
<string name="no_audio">Video pa audio</string>
<string name="download_archive_folder">Skedari i arkivës së shkarkimeve</string>
<string name="location">Vendi</string>
<string name="enable_alarm_permission">Duhet të aktivizoni lejen SCHEDULE_EXACT_ALARM në cilësimet e aplikacionit.</string>
<string name="process_downloads_background">Shkarkimet janë ende duke u ngarkuar. Vazhdoni t\'i përpunoni ato në sfond dhe të filloni shkarkimin?</string>
</resources> </resources>

View file

@ -24,6 +24,7 @@
<string-array name="video_containers"> <string-array name="video_containers">
<item>@string/defaultValue</item> <item>@string/defaultValue</item>
<item>mp4</item> <item>mp4</item>
<item>webm</item>
<item>mkv</item> <item>mkv</item>
<item>avi</item> <item>avi</item>
<item>flv</item> <item>flv</item>
@ -33,6 +34,7 @@
<string-array name="video_containers_values"> <string-array name="video_containers_values">
<item>Default</item> <item>Default</item>
<item>mp4</item> <item>mp4</item>
<item>webm</item>
<item>mkv</item> <item>mkv</item>
<item>avi</item> <item>avi</item>
<item>flv</item> <item>flv</item>

View file

@ -401,5 +401,5 @@
<string name="download_archive_folder">Download Archive Folder</string> <string name="download_archive_folder">Download Archive Folder</string>
<string name="location">Location</string> <string name="location">Location</string>
<string name="enable_alarm_permission">You need to enable the SCHEDULE_EXACT_ALARM permission in the app settings.</string> <string name="enable_alarm_permission">You need to enable the SCHEDULE_EXACT_ALARM permission in the app settings.</string>
<string name="process_downloads_background">Downloads are still loading. Continue procesing them in the background and start downloading?</string> <string name="process_downloads_background">"Downloads are still loading. Continue processing them in the background and start downloading? "</string>
</resources> </resources>

View file

@ -59,5 +59,4 @@
<style name="PreferenceTheme" parent="PreferenceThemeOverlay"> <style name="PreferenceTheme" parent="PreferenceThemeOverlay">
<item name="singleLineTitle">false</item> <item name="singleLineTitle">false</item>
</style> </style>
</resources> </resources>

View file

@ -29,7 +29,7 @@
app:title="@string/quick_download" /> app:title="@string/quick_download" />
<ListPreference <ListPreference
android:defaultValue="" android:defaultValue="url_type"
android:entries="@array/prevent_duplicate_downloads" android:entries="@array/prevent_duplicate_downloads"
android:entryValues="@array/prevent_duplicate_downloads_values" android:entryValues="@array/prevent_duplicate_downloads_values"
android:icon="@drawable/baseline_archive_24" android:icon="@drawable/baseline_archive_24"

View file

@ -59,14 +59,14 @@
android:icon="@drawable/ic_textformat" android:icon="@drawable/ic_textformat"
app:key="file_name_template" app:key="file_name_template"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:defaultValue="%(uploader)s - %(title)s" app:defaultValue="%(uploader)30B - %(title)120B"
app:title="@string/file_name_template" /> app:title="@string/file_name_template" />
<Preference <Preference
android:icon="@drawable/ic_textformat" android:icon="@drawable/ic_textformat"
app:key="file_name_template_audio" app:key="file_name_template_audio"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:defaultValue="%(uploader)s - %(title)s" app:defaultValue="%(uploader)30B - %(title)120B"
app:title="@string/file_name_template" /> app:title="@string/file_name_template" />
<SwitchPreferenceCompat <SwitchPreferenceCompat

View file

@ -181,7 +181,7 @@
app:title="@string/video_format" /> app:title="@string/video_format" />
<ListPreference <ListPreference
android:defaultValue="avc|h264" android:defaultValue=""
android:entries="@array/video_codec" android:entries="@array/video_codec"
android:entryValues="@array/video_codec_values" android:entryValues="@array/video_codec_values"
android:icon="@drawable/ic_code" android:icon="@drawable/ic_code"
@ -190,7 +190,7 @@
app:title="@string/preferred_video_codec" /> app:title="@string/preferred_video_codec" />
<ListPreference <ListPreference
android:defaultValue="m4a|mp4a|aac" android:defaultValue=""
android:entries="@array/audio_codec" android:entries="@array/audio_codec"
android:entryValues="@array/audio_codec_values" android:entryValues="@array/audio_codec_values"
android:icon="@drawable/ic_code" android:icon="@drawable/ic_code"