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

View file

@ -21,7 +21,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads ORDER BY status")
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>>
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
@ -38,12 +38,9 @@ interface DownloadDao {
@Query("""
SELECT COUNT(*) FROM downloads WHERE status = 'Processing'
UNION
SELECT COUNT(*) FROM downloads WHERE status ='Processing' AND type =
(SELECT type from downloads WHERE status = 'Processing' ORDER BY id LIMIT 1)
SELECT DISTINCT type from downloads where status = 'Processing' and id in (:ids)
""")
fun getProcessingDownloadsCountByType() : List<Int>
fun getProcessingDownloadTypes(ids: List<Long>) : List<String>
@Query("UPDATE downloads set status = 'Processing' WHERE id in (:ids)")
@ -53,7 +50,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status = 'Processing' ORDER BY id LIMIT 1")
fun getFirstProcessingDownload() : DownloadItem
@Query("SELECT * FROM downloads WHERE status in('Active','ActivePaused')")
@Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveAndPausedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
@ -62,32 +59,31 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Active'")
suspend fun getActiveDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused', 'Scheduled')")
@Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')")
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>
@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>>
@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>
@Query("SELECT format FROM downloads WHERE status in('Queued','QueuedPaused')")
@Query("SELECT format FROM downloads WHERE status='Queued'")
fun getSelectedFormatFromQueued() : List<Format>
@Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status in ('Queued', 'Scheduled') ORDER BY downloadStartTime, id LIMIT 20")
fun getQueuedScheduledDownloadsUntil(currentTime: Long) : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status in ('Queued','QueuedPaused','ActivePaused') ORDER BY downloadStartTime, id")
fun getQueuedAndPausedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id")
@Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id")
fun getQueuedDownloadsList() : List<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>
@RewriteQueriesToDropUnusedColumns
@ -133,22 +129,6 @@ interface DownloadDao {
@Query("SELECT status FROM downloads WHERE id=:id")
fun checkStatus(id: Long) : DownloadRepository.Status?
@Query("UPDATE downloads " +
"SET status = CASE " +
" WHEN status = 'Active' THEN 'ActivePaused' " +
" WHEN status = 'Queued' THEN 'QueuedPaused' " +
" ELSE status " +
" END;")
fun pauseActiveAndQueued()
@Query("UPDATE downloads " +
"SET status = CASE " +
" WHEN status = 'ActivePaused' THEN 'Active' " +
" WHEN status = 'QueuedPaused' THEN 'Queued' " +
" ELSE status " +
" END;")
fun unPauseActiveAndQueued()
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(item: DownloadItem) : Long
@ -179,7 +159,7 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE id in (:list)")
suspend fun deleteAllWithIDs(list: List<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()
@Query("DELETE FROM downloads WHERE status='Processing' AND id=:id")
@ -188,6 +168,9 @@ interface DownloadDao {
@Upsert
suspend fun update(item: DownloadItem)
@Query("UPDATE downloads set status=:status where id=:id")
suspend fun setStatus(id: Long, status: String)
@Update
suspend fun updateWithoutUpsert(item: DownloadItem)
@ -233,14 +216,11 @@ interface DownloadDao {
@Query("UPDATE downloads SET downloadStartTime=0, status='Queued' where id in (:list)")
suspend fun resetScheduleTimeForItems(list: List<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)")
suspend fun reQueueDownloadItems(list: List<Long>)
@Query("Update downloads SET status='Saved' WHERE status='Processing'")
fun updateProcessingtoSavedStatus()
@Query("Update downloads SET status='Saved' WHERE status='Processing' and id in (:ids)")
fun updateProcessingtoSavedStatus(ids: List<Long>)
@Transaction
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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import java.io.File
import java.util.concurrent.TimeUnit
@ -59,17 +60,15 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
)
val activeDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active).toListString())
val activeAndActivePausedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.ActivePaused).toListString())
val queuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Queued, Status.QueuedPaused).toListString())
val activeQueuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.ActivePaused, Status.Queued, Status.QueuedPaused).toListString())
val queuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Queued).toListString())
val activeQueuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.Queued).toListString())
val cancelledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Cancelled).toListString())
val erroredDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Error).toListString())
val savedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Saved).toListString())
val pausedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.ActivePaused, Status.QueuedPaused).toListString())
val scheduledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Scheduled).toListString())
enum class Status {
Active, ActivePaused, Queued, QueuedPaused, Error, Cancelled, Saved, Processing, Scheduled
Active, ActivePaused, Queued, Error, Cancelled, Saved, Processing, Scheduled
}
suspend fun insert(item: DownloadItem) : Long {
@ -102,9 +101,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
}
suspend fun setDownloadStatus(item: DownloadItem, status: Status){
item.status = status.toString()
update(item)
suspend fun setDownloadStatus(id: Long, status: Status){
downloadDao.setStatus(id, status.toString())
}
fun getItemByID(id: Long) : DownloadItem {
@ -184,14 +182,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.cancelActiveQueued()
}
fun pauseDownloads(){
downloadDao.pauseActiveAndQueued()
}
fun unPauseDownloads(){
downloadDao.unPauseActiveAndQueued()
}
fun removeLogID(logID: Long){
downloadDao.removeLogID(logID)
}
@ -200,6 +190,13 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.removeAllLogID()
}
fun getFilteredProcessingDownloads(ids: List<Long>) : Flow<List<DownloadItem>> {
return downloadDao.getProcessingDownloads()
.map {
it.filter { ids.contains(it.id) }
}
}
@SuppressLint("RestrictedApi")
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 {
var type = SourceType.SEARCH_QUERY
val p = Pattern.compile("((^(https?)://)?(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val m = p.matcher(inputQuery)
if (m.find()) {
type = SourceType.YOUTUBE_VIDEO

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -323,6 +323,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
getAlsoAudioDownloadItem{ audioDownloadItem ->
audioDownloadItem.downloadStartTime = it.timeInMillis
audioDownloadItem.status = DownloadRepository.Status.Scheduled.toString()
itemsToQueue.add(audioDownloadItem)
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.InfoUtil
import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.Shimmer
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
@ -66,6 +68,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@ -73,6 +76,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.function.Predicate
class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), ConfigureMultipleDownloadsAdapter.OnItemClickListener, View.OnClickListener,
ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
@ -87,12 +91,17 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var bottomAppBar: BottomAppBar
private lateinit var filesize : TextView
private lateinit var count : TextView
private lateinit var title: TextView
private lateinit var subtitle: TextView
private lateinit var shimmerTitle: ShimmerFrameLayout
private lateinit var shimmerSubtitle: ShimmerFrameLayout
private lateinit var sharedPreferences: SharedPreferences
private lateinit var currentDownloadIDs: List<Long>
private var processingDownloadsJobID: Long = 0
private var processingItemsCount : Int = 0
private lateinit var jobData: DownloadViewModel.ProcessingItemsJob
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
@ -103,7 +112,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
infoUtil = InfoUtil(requireContext())
currentDownloadIDs = arguments?.getLongArray("currentDownloadIDs")?.toList() ?: listOf()
processingDownloadsJobID = arguments?.getLong("processingDownloadsJobID") ?: 0
processingItemsCount = currentDownloadIDs.size
}
@ -150,14 +158,76 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_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)
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{
fun initSchedule() {
fun initSchedule(processingFinished: Boolean) {
UiUtil.showDatePicker(parentFragmentManager) { cal ->
scheduleBtn.isEnabled = false
download.isEnabled = false
@ -167,11 +237,9 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
downloadViewModel.deleteAllWithID(currentDownloadIDs)
}
val jobData = withContext(Dispatchers.IO){
downloadViewModel.getLoadingProcessingDownloadJob(processingDownloadsJobID)
}
jobData?.job?.cancel(CancellationException())
downloadProcessDownloadsInBackground(jobData, cal.timeInMillis)
jobData.job?.cancel(CancellationException())
jobData.job = null
downloadProcessDownloadsInBackground(jobData, processingFinished)
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()
@ -181,22 +249,18 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
}
if (progressBar.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
if (shimmerTitle.isVisible){
continueInBackgroundSnackBar.setAction(R.string.ok) {
initSchedule()
initSchedule(false)
}
continueInBackgroundSnackBar.show()
}else{
initSchedule()
initSchedule(true)
}
}
download!!.setOnClickListener {
fun initDownload() {
fun initDownload(processingFinished: Boolean) {
scheduleBtn.isEnabled = false
download.isEnabled = false
lifecycleScope.launch {
@ -204,26 +268,20 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
downloadViewModel.deleteAllWithID(currentDownloadIDs)
}
val jobData = withContext(Dispatchers.IO){
downloadViewModel.getLoadingProcessingDownloadJob(processingDownloadsJobID)
}
jobData?.job?.cancel(CancellationException())
downloadProcessDownloadsInBackground(jobData)
jobData.job?.cancel(CancellationException())
jobData.job = null
downloadProcessDownloadsInBackground(jobData, processingFinished)
dismiss()
}
}
if (progressBar.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
if (shimmerTitle.isVisible){
continueInBackgroundSnackBar.setAction(R.string.ok) {
initDownload()
initDownload(false)
}
continueInBackgroundSnackBar.show()
}else{
initDownload()
initDownload(true)
}
@ -237,8 +295,10 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch{
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.moveProcessingToSavedCategory()
downloadViewModel.moveProcessingToSavedCategory(jobData.processingDownloadItemIDs)
}
jobData.job?.cancel(CancellationException())
jobData.job = null
dismiss()
}
}
@ -246,19 +306,16 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
true
}
bottomAppBar = view.findViewById(R.id.bottomAppBar)
val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type)
val formatListener = object : OnFormatClickListener {
override fun onFormatClick(selectedFormats: List<FormatTuple>) {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingFormat(selectedFormats)
downloadViewModel.updateProcessingFormat(jobData.processingDownloadItemIDs, selectedFormats)
}
}
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingAllFormats(allFormats)
downloadViewModel.updateProcessingAllFormats(jobData.processingDownloadItemIDs, allFormats)
}
}
@ -266,8 +323,10 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
override fun onContinueOnBackground() {
requireActivity().lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.continueUpdatingFormatsOnBackground()
downloadViewModel.continueUpdatingFormatsOnBackground(jobData.processingDownloadItemIDs)
}
jobData.job?.cancel(CancellationException())
jobData.job = null
dismiss()
}
}
@ -305,7 +364,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
audio!!.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingType(DownloadViewModel.Type.audio)
downloadViewModel.updateProcessingType(jobData.processingDownloadItemIDs, DownloadViewModel.Type.audio)
withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
@ -317,7 +376,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
video!!.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch{
downloadViewModel.updateProcessingType(DownloadViewModel.Type.video)
downloadViewModel.updateProcessingType(jobData.processingDownloadItemIDs, DownloadViewModel.Type.video)
withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
@ -329,7 +388,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
command!!.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch{
downloadViewModel.updateProcessingType(DownloadViewModel.Type.command)
downloadViewModel.updateProcessingType(jobData.processingDownloadItemIDs, DownloadViewModel.Type.command)
withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
bottomAppBar.menu[1].icon?.alpha = 255
@ -353,7 +412,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
R.id.format -> {
lifecycleScope.launch {
val res = withContext(Dispatchers.IO){
downloadViewModel.checkIfAllProcessingItemsHaveSameType()
downloadViewModel.checkIfAllProcessingItemsHaveSameType(jobData.processingDownloadItemIDs,)
}
if (!res.first){
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 {
downloadViewModel.updateProcessingCommandFormat(format)
downloadViewModel.updateProcessingCommandFormat(jobData.processingDownloadItemIDs, format)
}
}
}else{
val items = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloads()
downloadViewModel.getAllByIDs(jobData.processingDownloadItemIDs)
}
val flatFormatCollection = items.map { it.allFormats }.flatten()
val commonFormats = withContext(Dispatchers.IO){
@ -401,7 +460,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
R.id.more -> {
lifecycleScope.launch {
val res = withContext(Dispatchers.IO){
downloadViewModel.checkIfAllProcessingItemsHaveSameType()
downloadViewModel.checkIfAllProcessingItemsHaveSameType(jobData.processingDownloadItemIDs)
}
if (!res.first) {
Toast.makeText(
@ -422,7 +481,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding)
val items = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloads()
downloadViewModel.getAllByIDs(jobData.processingDownloadItemIDs)
}
UiUtil.configureAudio(
@ -503,7 +562,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding)
val items = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloads()
downloadViewModel.getAllByIDs(jobData.processingDownloadItemIDs)
}
UiUtil.configureVideo(
@ -598,64 +657,31 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
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
progressBar.isVisible = loading
bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading }
private fun toggleLoadingShimmerTitle(show: Boolean) {
shimmerTitle.isVisible = show
title.isVisible = !show
shimmerSubtitle.isVisible = show
subtitle.isVisible = !show
listAdapter.submitList(it)
withContext(Dispatchers.Main){
updateFileSize(it.map { it2 -> it2.format.filesize })
}
if (it.isNotEmpty()){
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 -> {}
}
}
}
}
}
if (show){
shimmerTitle.startShimmer()
shimmerSubtitle.startShimmer()
}else{
shimmerTitle.stopShimmer()
shimmerSubtitle.stopShimmer()
}
}
private fun updateFileSize(items: List<Long>){
if (items.all { it > 0L }){
filesize.visibility = View.VISIBLE
filesize.text = "${getString(R.string.file_size)}: >~ ${FileUtil.convertFileSize(items.sum())}"
if (items.all { it > 5L }){
val size = FileUtil.convertFileSize(items.sum())
if (size != "?"){
filesize.visibility = View.VISIBLE
filesize.text = "${getString(R.string.file_size)}: >~ $size"
}
}else{
filesize.visibility = View.GONE
}
@ -674,7 +700,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
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())
@ -802,13 +828,14 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
listAdapter.notifyDataSetChanged()
}
}
override fun onDismiss(dialog: DialogInterface) {
downloadViewModel.cancelLoadingProcessingDownloads(processingDownloadsJobID)
downloadViewModel.deleteProcessing()
if (jobData.job != null){
jobData.job?.cancel(CancellationException())
downloadViewModel.deleteAllWithID(jobData.processingDownloadItemIDs)
}
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 ->
jobData?.apply {
intent.putExtra("itemType", jobData.itemType)
intent.putExtra("itemIDs", jobData.itemIDs.toLongArray())
intent.putExtra("timeInMillis", timeInMillis)
}
intent.putExtra("itemType", jobData.originItemType)
intent.putExtra("itemIDs", jobData.originItemIDs.toLongArray())
intent.putExtra("processingItemIDs", jobData.processingDownloadItemIDs.toLongArray())
intent.putExtra("timeInMillis", timeInMillis)
intent.putExtra("processingFinished", processingFinished)
requireActivity().startService(intent)
}
}

View file

@ -88,7 +88,21 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO){
if (currentDownloadItem != null){
if (savedInstanceState?.containsKey("updated") == true) {
downloadItem.apply {
title = resultItem!!.title
author = resultItem!!.author
allFormats = resultItem!!.formats
format = downloadViewModel.getFormat(allFormats, Type.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)
Gson().fromJson(string, DownloadItem::class.java)
}else{

View file

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

View file

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

View file

@ -2,21 +2,13 @@ package com.deniscerri.ytdl.ui.downloads
import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.graphics.Canvas
import android.graphics.Color
import android.content.SharedPreferences
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
@ -24,38 +16,30 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdl.ui.adapter.QueuedDownloadAdapter
import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.deniscerri.ytdl.work.DownloadWorker
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener {
@ -71,6 +55,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
private lateinit var resume: ExtendedFloatingActionButton
private lateinit var noResults: RelativeLayout
private lateinit var workManager: WorkManager
private lateinit var preferences: SharedPreferences
override fun onCreateView(
@ -90,6 +75,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
notificationUtil = NotificationUtil(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
workManager = WorkManager.getInstance(requireContext())
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
activeDownloads = ActiveDownloadAdapter(this,requireActivity())
activeRecyclerView = view.findViewById(R.id.download_recyclerview)
@ -105,102 +91,45 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
pause.setOnClickListener {
lifecycleScope.launch {
workManager.cancelAllWorkByTag("download")
pause.isEnabled = false
// pause queued
withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}.forEach {
it.status = DownloadRepository.Status.QueuedPaused.toString()
downloadViewModel.updateDownload(it)
}
// pause active
withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}.forEach {
cancelItem(it.id.toInt())
it.status = DownloadRepository.Status.ActivePaused.toString()
downloadViewModel.updateDownload(it)
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
}
preferences.edit().putBoolean("paused_downloads", true).apply()
activeDownloads.notifyDataSetChanged()
pause.isEnabled = true
resume.isVisible = true
pause.isVisible = false
}
}
resume.setOnClickListener {
lifecycleScope.launch {
resume.isEnabled = false
val active = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}
val toQueue = active.filter { it.status == DownloadRepository.Status.ActivePaused.toString() }.toMutableList()
runBlocking {
toQueue.forEach {
it.status = DownloadRepository.Status.Queued.toString()
downloadViewModel.updateDownload(it)
}
}
runBlocking {
preferences.edit().putBoolean("paused_downloads", false).apply()
resume.isVisible = false
pause.isVisible = true
withContext(Dispatchers.IO) {
downloadViewModel.resetActivePaused()
downloadViewModel.startDownloadWorker(listOf())
}
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) {}
withContext(Dispatchers.Main){
activeDownloads.notifyDataSetChanged()
}
}
}
lifecycleScope.launch {
downloadViewModel.activeAndActivePausedDownloadsCount.collectLatest {
noResults.isVisible = it == 0
}
}
lifecycleScope.launch {
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 {
downloadViewModel.pausedDownloadsCount.collectLatest {
resume.isVisible = it > 0
downloadViewModel.activeQueuedDownloadsCount.collectLatest {
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) {
lifecycleScope.launch {
val count = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}.count()
YoutubeDL.getInstance().destroyProcessById(itemID.toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
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 list = downloadViewModel.getQueued().toMutableList()
list.map { it.status = DownloadRepository.Status.Queued.toString() }
list
downloadViewModel.getQueued()
}
runBlocking {
downloadViewModel.queueDownloads(queue)
if (!preferences.getBoolean("paused_downloads", false)){
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) {
@ -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")
reconfigureID?.apply {
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(reconfigureID)
kotlin.runCatching {
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 -> {
lifecycleScope.launch {
val tabStatus = mapOf(
0 to listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused),
1 to listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused),
0 to listOf(DownloadRepository.Status.Active),
1 to listOf(DownloadRepository.Status.Queued),
2 to listOf(DownloadRepository.Status.Scheduled),
3 to listOf(DownloadRepository.Status.Cancelled),
4 to listOf(DownloadRepository.Status.Error),
@ -263,11 +265,10 @@ class DownloadQueueMainFragment : Fragment(){
}
private fun cancelAllDownloads() {
cancelBackgroundProcessingDownloads()
workManager.cancelAllWorkByTag("download")
lifecycleScope.launch {
val notificationUtil = NotificationUtil(requireContext())
downloadViewModel.cancelActiveQueued()
cancelBackgroundProcessingDownloads()
val activeAndQueued = withContext(Dispatchers.IO){
downloadViewModel.getActiveAndQueuedDownloadIDs()
}
@ -275,6 +276,7 @@ class DownloadQueueMainFragment : Fragment(){
YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id.toInt())
}
downloadViewModel.cancelActiveQueued()
}
}
@ -296,6 +298,7 @@ class DownloadQueueMainFragment : Fragment(){
}
Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent ->
intent.putExtra("binding", true)
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
noResults.isVisible = it == 0
dragHandleToggle.isVisible = it > 1
@ -190,7 +190,7 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
val selectedIDs = getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused).toListString())
DownloadRepository.Status.Queued).toListString())
}.toMutableList()
idsInMiddle.addAll(selectedIDs)
if (idsInMiddle.isNotEmpty()){
@ -294,7 +294,7 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
return if (adapter.inverted || adapter.checkedItems.isEmpty()){
withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(adapter.checkedItems.toList(), listOf(
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused))
DownloadRepository.Status.Queued))
}
}else{
adapter.checkedItems.toList()
@ -475,12 +475,18 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
}
},
longClickDownloadButton = {
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
Pair("downloadItem", it),
Pair("result", downloadViewModel.createResultItemFromDownload(it)),
Pair("type", it.type)
)
)
lifecycleScope.launch {
it.status = DownloadRepository.Status.Saved.toString()
withContext(Dispatchers.IO){
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 ->
UiUtil.showDatePicker(parentFragmentManager) {
@ -517,7 +523,7 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
if(selectedObjects == 2){
val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused).toListString())
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Queued).toListString())
}
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 ->
val itemId = m.itemId
if (itemId == R.id.remove_logs) {
throw Exception("asffffffffff")
try{
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.confirm_delete_history))

View file

@ -80,7 +80,9 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
withContext(Dispatchers.IO){
updateUtil!!.updateApp{ msg ->
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 {
val msg = it.message ?: requireContext().getString(R.string.errored)
val snackBar = Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG)
snackBar.setAction(R.string.copy_log){
UiUtil.copyToClipboard(msg, requireActivity())
view?.apply {
val snackBar = Snackbar.make(this, msg, Snackbar.LENGTH_LONG)
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.ui.adapter.TerminalDownloadsAdapter
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.progressindicator.LinearProgressIndicator
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
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) {
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.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.DownloadWorker
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.slider.Slider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import kotlin.properties.Delegates

View file

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

View file

@ -467,7 +467,6 @@ class InfoUtil(private val context: Context) {
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--compat-options", "manifest-filesize-approx")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
@ -993,6 +992,8 @@ class InfoUtil(private val context: Context) {
request.addOption("--no-resize-buffer")
}
request.addOption("--socket-timeout", 60)
val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!!
if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL)
@ -1059,7 +1060,7 @@ class InfoUtil(private val context: Context) {
}
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
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)){
audioQualityId = "wa/w"
}
@ -1149,7 +1150,7 @@ class InfoUtil(private val context: Context) {
if(ext.isNotBlank()){
if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(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()) {
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 {
request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
}
@ -1261,6 +1262,7 @@ class InfoUtil(private val context: Context) {
val f = StringBuilder()
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 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 {
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 (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref")
if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref")
if (cont.isNotBlank()) append(",ext:$cont")
if (cont.isNotBlank()) append(",ext:$cont:")
if (this.isNotBlank()){
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 formats = mutableListOf<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()){
resources.getString(R.string.defaultValue)
}else{
@ -1508,14 +1516,14 @@ class InfoUtil(private val context: Context) {
val videoFormats = resources.getStringArray(R.array.video_formats_values)
val formats = mutableListOf<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()){
val audioCodecs = resources.getStringArray(R.array.audio_codec)
val audioCodecsValues = resources.getStringArray(R.array.audio_codec_values)
audioCodecs[audioCodecsValues.indexOf(this)]
}else this
}
val videoCodecPreference = sharedPreferences.getString("video_codec", "avc|h264")!!.run {
val videoCodecPreference = sharedPreferences.getString("video_codec", "")!!.run {
if (this.isEmpty()){
resources.getString(R.string.defaultValue)
}else{

View file

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

View file

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

View file

@ -39,6 +39,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import java.io.File
import java.util.Locale
import kotlin.random.Random
@ -88,6 +89,7 @@ class DownloadWorker(
setForegroundAsync(foregroundInfo)
queuedItems.collect { items ->
if (isStopped) return@collect
runningYTDLInstances.clear()
val activeDownloads = dao.getActiveDownloadsList()
activeDownloads.forEach {
@ -120,7 +122,7 @@ class DownloadWorker(
//update item if its incomplete
resultRepo.updateDownloadItem(downloadItem)?.apply {
val status = dao.checkStatus(this.id)
if (listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused).contains(status)){
if (status == DownloadRepository.Status.Active){
dao.updateWithoutUpsert(this)
}
}
@ -158,9 +160,11 @@ class DownloadWorker(
dao.update(downloadItem)
}
val eventBus = EventBus.getDefault()
runCatching {
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 }
notificationUtil.updateDownloadNotification(
downloadItem.id.toInt(),
@ -181,7 +185,7 @@ class DownloadWorker(
var finalPaths : MutableList<String>?
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")
finalPaths =
outputSequence.asSequence()
@ -206,16 +210,16 @@ class DownloadWorker(
}
}else{
//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 {
finalPaths = withContext(Dispatchers.IO){
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()
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{
finalPaths = mutableListOf(context.getString(R.string.unfound_file))
}
@ -285,7 +289,7 @@ class DownloadWorker(
if (wasQuickDownloaded && createResultItem){
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 {
infoUtil.getFromYTDL(downloadItem.url).forEach { res ->
if (res != null) {
@ -304,50 +308,46 @@ class DownloadWorker(
}
}.onFailure {
if (this@DownloadWorker.isStopped) return@onFailure
FileUtil.deleteConfigFiles(request)
withContext(Dispatchers.Main){
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 (logDownloads){
logRepo.update(it.message!!, logItem.id)
}else{
logString.append("${it.message}\n")
logItem.content = logString.toString()
val logID = logRepo.insert(logItem)
downloadItem.logID = logID
}
if(it.message != null){
if (logDownloads){
logRepo.update(it.message!!, logItem.id)
}else{
logString.append("${it.message}\n")
logItem.content = logString.toString()
val logID = logRepo.insert(logItem)
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"
}
class WorkerProgress(
val progress: Int,
val output: String,
val downloadItemID: Long
)
}

View file

@ -118,7 +118,7 @@ class ObserveSourceWorker(
}
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 parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->

View file

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

View file

@ -105,42 +105,72 @@
app:layout_constraintTop_toTopOf="parent"
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
android:id="@+id/search_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
app:layout_scrollFlags="scroll|enterAlways"
android:hint="@string/search_hint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/home_toolbar"
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>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
<RelativeLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
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">
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
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
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
android:layout_width="match_parent"
@ -222,6 +252,10 @@
</com.facebook.shimmer.ShimmerFrameLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/home_fabs"
@ -238,6 +272,7 @@
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="10dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />

View file

@ -91,148 +91,184 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_gravity="end"
app:layout_scrollFlags="scroll|enterAlways"
android:hint="@string/search_hint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/home_toolbar"
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/main_menu" />
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="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>
<androidx.recyclerview.widget.RecyclerView
<RelativeLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
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"
/>
android:layout_height="match_parent">
<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:id="@+id/recyclerViewHome"
android:orientation="vertical"
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">
<LinearLayout
android:layout_width="match_parent"
android:weightSum="3"
android:layout_height="200dp">
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:orientation="vertical">
<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
android:layout_width="match_parent"
android:weightSum="3"
android:layout_height="wrap_content">
</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
android:layout_width="match_parent"
android:weightSum="3"
android:layout_height="200dp">
</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
android:layout_width="match_parent"
android:weightSum="3"
android:layout_height="wrap_content">
</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
android:layout_width="match_parent"
android:weightSum="3"
android:layout_height="200dp">
</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
android:layout_width="match_parent"
android:weightSum="3"
android:layout_height="wrap_content">
</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
android:layout_width="match_parent"
android:weightSum="3"
android:layout_height="200dp">
</LinearLayout>
<LinearLayout
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>
</com.facebook.shimmer.ShimmerFrameLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/home_fabs"
@ -249,6 +285,7 @@
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="10dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />

View file

@ -105,123 +105,153 @@
app:layout_constraintTop_toTopOf="parent"
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
android:id="@+id/search_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
app:layout_scrollFlags="scroll|enterAlways"
android:hint="@string/search_hint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/home_toolbar"
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>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
<RelativeLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
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">
android:layout_height="match_parent">
<LinearLayout
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
android:orientation="vertical"
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">
<LinearLayout
android:layout_width="match_parent"
android:weightSum="2"
android:layout_height="250dp">
android:layout_height="wrap_content"
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
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>
</com.facebook.shimmer.ShimmerFrameLayout>
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/home_fabs"
@ -237,6 +267,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginBottom="10dp"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"

View file

@ -91,167 +91,203 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_gravity="end"
app:layout_scrollFlags="scroll|enterAlways"
android:hint="@string/search_hint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/home_toolbar"
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/main_menu" />
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="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>
<androidx.recyclerview.widget.RecyclerView
<RelativeLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
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"
/>
android:layout_height="match_parent">
<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:id="@+id/recyclerViewHome"
android:orientation="vertical"
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">
<LinearLayout
android:layout_width="match_parent"
android:weightSum="4"
android:layout_height="200dp"
android:baselineAligned="false">
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:orientation="vertical">
<include
android:layout_weight="1"
layout="@layout/result_card_shimmer"
<LinearLayout
android:layout_width="match_parent"
android:weightSum="4"
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" />
android:baselineAligned="false">
</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
android:layout_width="match_parent"
android:weightSum="4"
android:layout_height="200dp">
</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
android:layout_width="match_parent"
android:weightSum="4"
android:layout_height="wrap_content">
</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
android:layout_width="match_parent"
android:weightSum="4"
android:layout_height="200dp">
</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
android:layout_width="match_parent"
android:weightSum="4"
android:layout_height="wrap_content">
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:weightSum="4"
android:layout_height="200dp">
<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="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>
</com.facebook.shimmer.ShimmerFrameLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/home_fabs"
@ -268,6 +304,7 @@
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="10dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />

View file

@ -166,6 +166,23 @@
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
android:id="@+id/active_download_delete"
style="?attr/materialIconButtonFilledStyle"

View file

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

View file

@ -41,6 +41,29 @@
android:orientation="horizontal"
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
android:id="@+id/bottom_sheet_title"
android:layout_width="0dp"
@ -53,6 +76,28 @@
app:layout_constraintStart_toStartOf="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
android:id="@+id/bottom_sheet_subtitle"
@ -114,6 +159,7 @@
android:layout_height="wrap_content"
android:visibility="gone"
android:paddingEnd="20dp"
android:paddingStart="0dp"
android:paddingTop="10dp"
android:text="@string/file_size"
android:textStyle="bold"
@ -124,12 +170,6 @@
</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
android:id="@+id/downloadMultipleRecyclerview"
android:layout_width="match_parent"

View file

@ -1,5 +1,5 @@
<?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_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
@ -141,13 +141,45 @@
app:layout_scrollFlags="scroll|enterAlways"
android:layout_height="wrap_content" />
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
app:menu="@menu/main_menu"
app:layout_scrollFlags="scroll|enterAlways"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/search_hint" />
android:layout_marginVertical="15dp"
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>
@ -167,6 +199,7 @@
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="10dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />

View file

@ -1,112 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout android:layout_width="match_parent"
android:layout_height="230dp"
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"
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="20dp"
app:cardElevation="10dp"
app:cardMaxElevation="12dp"
app:cardBackgroundColor="@color/grey"
app:cardPreventCornerOverlap="true"
android:layout_margin="10dp"
<RelativeLayout android:layout_width="0dp"
android:layout_height="0dp"
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"
>
<androidx.appcompat.widget.AppCompatImageView
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
<androidx.cardview.widget.CardView
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">
app:cardCornerRadius="20dp"
app:cardElevation="10dp"
app:cardMaxElevation="12dp"
app:cardBackgroundColor="@color/grey"
app:cardPreventCornerOverlap="true"
android:layout_margin="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"
<androidx.appcompat.widget.AppCompatImageView
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" />
<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"
<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" />
</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>
</RelativeLayout>
<LinearLayout
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"
android:layout_gravity="bottom">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/from_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
android:layout_width="70dp"
android:layout_height="wrap_content"
<LinearLayout
android:layout_width="wrap_content"
android:layout_marginStart="50dp"
android:layout_marginEnd="10dp"
android:hint="@string/start">
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/from_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
android:layout_width="70dp"
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
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.TextInputLayout>
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
<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: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>

View file

@ -378,4 +378,24 @@
<string name="hour">Ora</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="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>

View file

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

View file

@ -401,5 +401,5 @@
<string name="download_archive_folder">Download Archive Folder</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="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>

View file

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

View file

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

View file

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

View file

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