multiple download card fixes

This commit is contained in:
zaednasr 2024-07-07 20:48:22 +02:00
parent fa2bd2ed14
commit 80266888e3
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
48 changed files with 1421 additions and 1972 deletions

View file

@ -130,10 +130,10 @@ dependencies {
implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer" implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer" implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer" implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer"
//
// implementation "io.github.junkfood02.youtubedl-android:library:0.16.0" // implementation "io.github.junkfood02.youtubedl-android:library:0.16.1"
// implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.16.0" // implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.16.1"
// implementation "io.github.junkfood02.youtubedl-android:aria2c:0.16.0" // implementation "io.github.junkfood02.youtubedl-android:aria2c:0.16.1"
implementation "androidx.appcompat:appcompat:$appCompatVer" implementation "androidx.appcompat:appcompat:$appCompatVer"
implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.constraintlayout:constraintlayout:2.1.4"
@ -143,10 +143,10 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation "androidx.navigation:navigation-fragment-ktx:$navVer" implementation "androidx.navigation:navigation-fragment-ktx:$navVer"
implementation "androidx.navigation:navigation-ui-ktx:$navVer" implementation "androidx.navigation:navigation-ui-ktx:$navVer"
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.test.ext:junit-ktx:1.1.5' implementation 'androidx.test.ext:junit-ktx:1.2.1'
implementation 'androidx.compose.ui:ui-android:1.6.5' implementation 'androidx.compose.ui:ui-android:1.6.8'
implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
testImplementation "junit:junit:$junitVer" testImplementation "junit:junit:$junitVer"
androidTestImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer"
androidTestImplementation "androidx.test.ext:junit:$androidJunitVer" androidTestImplementation "androidx.test.ext:junit:$androidJunitVer"
@ -167,10 +167,10 @@ dependencies {
implementation "androidx.room:room-runtime:$roomVer" implementation "androidx.room:room-runtime:$roomVer"
implementation "androidx.room:room-ktx:$roomVer" implementation "androidx.room:room-ktx:$roomVer"
ksp "androidx.room:room-compiler:$roomVer" ksp "androidx.room:room-compiler:$roomVer"
implementation 'androidx.paging:paging-runtime-ktx:3.2.1' implementation 'androidx.paging:paging-runtime-ktx:3.3.0'
implementation "androidx.room:room-paging:$roomVer" implementation "androidx.room:room-paging:$roomVer"
androidTestImplementation "androidx.room:room-testing:$roomVer" androidTestImplementation "androidx.room:room-testing:$roomVer"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.3'
implementation "androidx.compose.runtime:runtime:$composeVer" implementation "androidx.compose.runtime:runtime:$composeVer"
androidTestImplementation 'com.google.truth:truth:1.1.5' androidTestImplementation 'com.google.truth:truth:1.1.5'
@ -182,7 +182,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4' implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.2" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.3"
implementation "com.github.Irineu333:Highlight-KT:1.0.4" implementation "com.github.Irineu333:Highlight-KT:1.0.4"
// For media playback using ExoPlayer // For media playback using ExoPlayer

View file

@ -258,13 +258,10 @@ class MainActivity : BaseActivity() {
navigationView.visibilityChanged { navigationView.visibilityChanged {
if (it.isVisible){ if (it.isVisible){
val curr = navController.currentDestination?.id val curr = navController.currentDestination?.id
if (curr != R.id.homeFragment && curr != R.id.historyFragment && curr != R.id.moreFragment) hideBottomNavigation() if (!showingNavbarItems.contains(curr)) hideBottomNavigation()
} }
} }
when(preferences.getString("start_destination", "")) {
"Queue" -> if (savedInstanceState == null) navController.navigate(R.id.downloadQueueMainFragment)
}
cookieViewModel.updateCookiesFile() cookieViewModel.updateCookiesFile()
val intent = intent val intent = intent

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdl.database.dao package com.deniscerri.ytdl.database.dao
import android.util.Log
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
@ -25,7 +26,7 @@ interface DownloadDao {
fun getActiveDownloads() : Flow<List<DownloadItem>> fun getActiveDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status = 'Processing'") @Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloads() : Flow<List<DownloadItem>> fun getProcessingDownloads() : Flow<List<DownloadItemSimple>>
@Query("SELECT COUNT(*) FROM downloads WHERE status in (:statuses)") @Query("SELECT COUNT(*) FROM downloads WHERE status in (:statuses)")
fun getDownloadsCountFlow(statuses: List<String>) : Flow<Int> fun getDownloadsCountFlow(statuses: List<String>) : Flow<Int>
@ -54,15 +55,18 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status = 'Processing'") @Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloadsList() : List<DownloadItem> fun getProcessingDownloadsList() : List<DownloadItem>
@Query("UPDATE downloads set downloadStartTime=:time, status='Scheduled' WHERE status ='Processing'")
suspend fun updateProcessingDownloadTime(time: Long)
@Query("UPDATE downloads set downloadPath=:path WHERE status ='Processing'") @Query("UPDATE downloads set downloadPath=:path WHERE status ='Processing'")
suspend fun updateProcessingDownloadPath(path: String) suspend fun updateProcessingDownloadPath(path: String)
@Query("SELECT * FROM downloads WHERE status='Active'") @Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloadsList() : List<DownloadItem> fun getActiveDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE url=:url AND status='Processing'")
fun getProcessingDownloadsByUrl(url: String) : List<DownloadItem>
@Query("DELETE from downloads where status = 'Processing' AND url=:url")
suspend fun deleteProcessingByUrl(url: String)
@Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')") @Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')")
fun getActiveAndQueuedDownloadsList() : List<DownloadItem> fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@Query("UPDATE downloads SET status='Queued' where status = 'Active'") @Query("UPDATE downloads SET status='Queued' where status = 'Active'")
@ -179,6 +183,13 @@ interface DownloadDao {
@Upsert @Upsert
suspend fun update(item: DownloadItem) suspend fun update(item: DownloadItem)
@Transaction
suspend fun updateAll(list: List<DownloadItem>){
list.forEach {
update(it)
}
}
@Query("UPDATE downloads set status=:status where id=:id") @Query("UPDATE downloads set status=:status where id=:id")
suspend fun setStatus(id: Long, status: String) suspend fun setStatus(id: Long, status: String)

View file

@ -49,9 +49,15 @@ interface ResultDao {
@Query("DELETE FROM results WHERE id=:id") @Query("DELETE FROM results WHERE id=:id")
suspend fun delete(id: Long) suspend fun delete(id: Long)
@Query("DELETE FROM results WHERE url=:url")
suspend fun deleteByUrl(url: String)
@Query("SELECT * FROM results WHERE url=:url LIMIT 1") @Query("SELECT * FROM results WHERE url=:url LIMIT 1")
fun getResultByURL(url: String) : ResultItem? fun getResultByURL(url: String) : ResultItem?
@Query("SELECT * FROM results WHERE url=:url")
fun getAllByURL(url: String) : List<ResultItem>
@Query("SELECT * FROM results where id=:id LIMIT 1") @Query("SELECT * FROM results where id=:id LIMIT 1")
fun getResultByID(id: Long): ResultItem? fun getResultByID(id: Long): ResultItem?

View file

@ -26,6 +26,7 @@ import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.DownloadItemSimple import com.deniscerri.ytdl.database.models.DownloadItemSimple
import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.DownloadWorker import com.deniscerri.ytdl.work.DownloadWorker
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@ -42,7 +43,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
pagingSourceFactory = {downloadDao.getAllDownloads()} pagingSourceFactory = {downloadDao.getAllDownloads()}
) )
val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads().distinctUntilChanged() val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads().distinctUntilChanged()
val processingDownloads : Flow<List<DownloadItem>> = downloadDao.getProcessingDownloads().distinctUntilChanged() val processingDownloads : Flow<List<DownloadItemSimple>> = downloadDao.getProcessingDownloads().distinctUntilChanged()
val queuedDownloads : Pager<Int, DownloadItemSimple> = Pager( val queuedDownloads : Pager<Int, DownloadItemSimple> = Pager(
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
pagingSourceFactory = {downloadDao.getQueuedDownloads()} pagingSourceFactory = {downloadDao.getQueuedDownloads()}
@ -101,6 +102,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.update(item) downloadDao.update(item)
} }
suspend fun updateAll(list: List<DownloadItem>) {
downloadDao.updateAll(list)
}
suspend fun updateWithoutUpsert(item: DownloadItem){ suspend fun updateWithoutUpsert(item: DownloadItem){
kotlin.runCatching { downloadDao.updateWithoutUpsert(item) } kotlin.runCatching { downloadDao.updateWithoutUpsert(item) }
} }
@ -122,6 +127,14 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getActiveDownloadsList() return downloadDao.getActiveDownloadsList()
} }
fun getProcessingDownloadsByUrl(url: String) : List<DownloadItem> {
return downloadDao.getProcessingDownloadsByUrl(url)
}
suspend fun deleteProcessingByUrl(url: String) {
downloadDao.deleteProcessingByUrl(url)
}
fun getProcessingDownloads() : List<DownloadItem> { fun getProcessingDownloads() : List<DownloadItem> {
return downloadDao.getProcessingDownloadsList() return downloadDao.getProcessingDownloadsList()
} }
@ -130,10 +143,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getActiveAndQueuedDownloadsList() return downloadDao.getActiveAndQueuedDownloadsList()
} }
suspend fun updateProcessingDownloadTime(time: Long) {
downloadDao.updateProcessingDownloadTime(time)
}
fun getActiveAndQueuedDownloadIDs() : List<Long> { fun getActiveAndQueuedDownloadIDs() : List<Long> {
return downloadDao.getActiveAndQueuedDownloadIDs() return downloadDao.getActiveAndQueuedDownloadIDs()
} }
@ -203,14 +212,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.removeAllLogID() downloadDao.removeAllLogID()
} }
fun getFilteredProcessingDownloads(ids: List<Long>) : Flow<List<DownloadItem>> {
return downloadDao.getProcessingDownloads()
.map {
it.filter { ids.contains(it.id) }
}
}
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) { suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@ -230,12 +231,16 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
if (delay <= 60000L) delay = 0L if (delay <= 60000L) delay = 0L
} }
val useAlarmForScheduling = sharedPreferences.getBoolean("use_alarm_for_scheduling", false)
if (delay > 0L && useAlarmForScheduling) {
AlarmScheduler(context).scheduleAt(queuedItems.minBy { it.downloadStartTime }.downloadStartTime)
return
}
val workConstraints = Constraints.Builder() val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED) if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
//workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>() val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download") .addTag("download")
@ -243,10 +248,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
.setInitialDelay(delay, TimeUnit.MILLISECONDS) .setInitialDelay(delay, TimeUnit.MILLISECONDS)
.setInputData(inputData.build()) .setInputData(inputData.build())
queuedItems.forEach {
workRequest.addTag(it.id.toString())
}
workManager.enqueueUniqueWork( workManager.enqueueUniqueWork(
System.currentTimeMillis().toString(), System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE, ExistingWorkPolicy.REPLACE,

View file

@ -5,7 +5,9 @@ import android.util.Patterns
import com.deniscerri.ytdl.database.dao.ResultDao import com.deniscerri.ytdl.database.dao.ResultDao
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.ResultItem import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import java.util.regex.Pattern import java.util.regex.Pattern
@ -88,6 +90,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
if (tmpToken.isEmpty()) break if (tmpToken.isEmpty()) break
if (tmpToken == nextPageToken) break if (tmpToken == nextPageToken) break
nextPageToken = tmpToken nextPageToken = tmpToken
delay(1000)
} while (true) } while (true)
itemCount.value = items.size itemCount.value = items.size
return items return items
@ -117,6 +120,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
resultDao.delete(item.id) resultDao.delete(item.id)
} }
suspend fun deleteByUrl(url: String) {
resultDao.deleteByUrl(url)
}
suspend fun deleteAll(){ suspend fun deleteAll(){
itemCount.value = 0 itemCount.value = 0
resultDao.deleteAll() resultDao.deleteAll()
@ -134,6 +141,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return resultDao.getResultByURL(url) return resultDao.getResultByURL(url)
} }
fun getAllByURL(url: String) : List<ResultItem> {
return resultDao.getAllByURL(url)
}
fun getAllByIDs(ids: List<Long>) : List<ResultItem> { fun getAllByIDs(ids: List<Long>) : List<ResultItem> {
return resultDao.getAllByIDs(ids) return resultDao.getAllByIDs(ids)
} }
@ -162,9 +173,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
private fun getQueryType(inputQuery: String) : SourceType { private fun getQueryType(inputQuery: String) : SourceType {
var type = SourceType.SEARCH_QUERY var type = SourceType.SEARCH_QUERY
val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)") if (inputQuery.isYoutubeURL()) {
val m = p.matcher(inputQuery)
if (m.find()) {
type = SourceType.YOUTUBE_VIDEO type = SourceType.YOUTUBE_VIDEO
if (inputQuery.contains("playlist?list=")) { if (inputQuery.contains("playlist?list=")) {
type = SourceType.YOUTUBE_PLAYLIST type = SourceType.YOUTUBE_PLAYLIST

View file

@ -5,7 +5,13 @@ import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log
import android.widget.Toast
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MediatorLiveData
@ -34,18 +40,26 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ResultRepository import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.ui.downloadcard.FormatTuple import com.deniscerri.ytdl.ui.downloadcard.FormatTuple
import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple
import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatSorter
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker
import com.google.gson.Gson
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import okhttp3.internal.format
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
@ -53,14 +67,15 @@ import java.util.Locale
class DownloadViewModel(private val application: Application) : AndroidViewModel(application) { class DownloadViewModel(private val application: Application) : AndroidViewModel(application) {
private val dbManager: DBManager private val dbManager: DBManager
val repository : DownloadRepository val repository : DownloadRepository
private val sharedDownloadViewModel: SharedDownloadViewModel
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao private val commandTemplateDao: CommandTemplateDao
private val infoUtil : InfoUtil private val infoUtil : InfoUtil
private val resources : Resources
val allDownloads : Flow<PagingData<DownloadItem>> val allDownloads : Flow<PagingData<DownloadItem>>
val queuedDownloads : Flow<PagingData<DownloadItemSimple>> val queuedDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloads : Flow<List<DownloadItem>> val activeDownloads : Flow<List<DownloadItem>>
val processingDownloads : Flow<List<DownloadItem>> val processingDownloads : Flow<List<DownloadItemSimple>>
val cancelledDownloads : Flow<PagingData<DownloadItemSimple>> val cancelledDownloads : Flow<PagingData<DownloadItemSimple>>
val erroredDownloads : Flow<PagingData<DownloadItemSimple>> val erroredDownloads : Flow<PagingData<DownloadItemSimple>>
val savedDownloads : Flow<PagingData<DownloadItemSimple>> val savedDownloads : Flow<PagingData<DownloadItemSimple>>
@ -74,7 +89,15 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val savedDownloadsCount : Flow<Int> val savedDownloadsCount : Flow<Int>
val scheduledDownloadsCount : Flow<Int> val scheduledDownloadsCount : Flow<Int>
val alreadyExistsUiState: MutableStateFlow<List<SharedDownloadViewModel.AlreadyExistsIDs>> @Parcelize
data class AlreadyExistsIDs(
var downloadItemID: Long,
var historyItemID : Long?
) : Parcelable
val alreadyExistsUiState: MutableStateFlow<List<AlreadyExistsIDs>> = MutableStateFlow(
mutableListOf()
)
private var extraCommandsForAudio: String = "" private var extraCommandsForAudio: String = ""
private var extraCommandsForVideo: String = "" private var extraCommandsForVideo: String = ""
@ -87,15 +110,19 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
auto, audio, video, command auto, audio, video, command
} }
var processingItemsFlow : ProcessingItemsJob? = null private val urlsForAudioType = listOf(
"music",
"audio",
"soundcloud"
)
var processingItems = MutableStateFlow(false) var processingItems = MutableStateFlow(false)
var processingItemsJob : Job? = null
init { init {
dbManager = DBManager.getInstance(application) dbManager = DBManager.getInstance(application)
dao = dbManager.downloadDao dao = dbManager.downloadDao
repository = DownloadRepository(dao) repository = DownloadRepository(dao)
sharedDownloadViewModel = SharedDownloadViewModel(application)
alreadyExistsUiState = sharedDownloadViewModel.alreadyExistsUiState
historyRepository = HistoryRepository(dbManager.historyDao) historyRepository = HistoryRepository(dbManager.historyDao)
resultRepository = ResultRepository(dbManager.resultDao, application) resultRepository = ResultRepository(dbManager.resultDao, application)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
@ -125,6 +152,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
} }
val confTmp = Configuration(application.resources.configuration)
confTmp.setLocale(Locale(sharedPreferences.getString("app_language", "en")!!))
val metrics = DisplayMetrics()
resources = Resources(application.assets, metrics, confTmp)
} }
fun deleteDownload(id: Long) = viewModelScope.launch(Dispatchers.IO) { fun deleteDownload(id: Long) = viewModelScope.launch(Dispatchers.IO) {
@ -155,15 +187,130 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun getDownloadType(t: Type? = null, url: String) : Type { fun getDownloadType(t: Type? = null, url: String) : Type {
return sharedDownloadViewModel.getDownloadType(t, url) var type = t
if (type == null){
val preferredDownloadType = sharedPreferences.getString("preferred_download_type", Type.auto.toString())
type = if (sharedPreferences.getBoolean("remember_download_type", false)){
Type.valueOf(sharedPreferences.getString("last_used_download_type",
preferredDownloadType)!!)
}else{
Type.valueOf(preferredDownloadType!!)
}
}
return when(type){
Type.auto -> {
if (urlsForAudioType.any { url.contains(it) }){
Type.audio
}else{
Type.video
}
}
else -> type
}
} }
fun createDownloadItemFromResult(result: ResultItem?, url: String = "", givenType: Type) : DownloadItem { fun createDownloadItemFromResult(result: ResultItem?, url: String = "", givenType: Type) : DownloadItem {
return sharedDownloadViewModel.createDownloadItemFromResult(result, url, givenType) val resultItem = result ?: createEmptyResultItem(url)
val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
val saveAutoSubs = sharedPreferences.getBoolean("write_auto_subtitles", false)
val addChapters = sharedPreferences.getBoolean("add_chapters", false)
val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
val cropThumb = sharedPreferences.getBoolean("crop_thumbnail", false)
var type = getDownloadType(givenType, resultItem.url)
if(type == Type.command && commandTemplateDao.getTotalNumber() == 0) type = Type.video
val customFileNameTemplate = when(type) {
Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader).30B - %(title).170B")
Type.video -> sharedPreferences.getString("file_name_template", "%(uploader).30B - %(title).170B")
else -> ""
}
val downloadPath = when(type){
Type.audio -> sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())
Type.video -> sharedPreferences.getString("video_path", FileUtil.getDefaultVideoPath())
else -> sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
}
val container = when(type){
Type.audio -> sharedPreferences.getString("audio_format", "")
else -> sharedPreferences.getString("video_format", "")
}
val sponsorblock = sharedPreferences.getStringSet("sponsorblock_filters", emptySet())
val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!))
val preferredAudioFormats = getPreferredAudioFormats(resultItem.formats)
val videoPreferences = VideoPreferences(
embedSubs,
addChapters, false,
ArrayList(sponsorblock),
saveSubs,
saveAutoSubs,
audioFormatIDs = preferredAudioFormats
)
val extraCommands = when(type){
Type.audio -> extraCommandsForAudio
Type.video -> extraCommandsForVideo
else -> ""
}
return DownloadItem(0,
resultItem.url,
resultItem.title,
resultItem.author,
resultItem.thumb,
resultItem.duration,
type,
getFormat(resultItem.formats, type),
container!!,
"",
resultItem.formats,
downloadPath!!, resultItem.website,
"",
if (resultItem.playlistTitle == resultRepository.YTDLNIS_SEARCH) "" else resultItem.playlistTitle,
audioPreferences,
videoPreferences,
extraCommands,
customFileNameTemplate!!,
saveThumb,
DownloadRepository.Status.Queued.toString(),
0,
null,
playlistURL = resultItem.playlistURL,
playlistIndex = resultItem.playlistIndex,
incognito = sharedPreferences.getBoolean("incognito", false)
)
} }
fun createResultItemFromDownload(downloadItem: DownloadItem) : ResultItem { fun createResultItemFromDownload(downloadItem: DownloadItem) : ResultItem {
return sharedDownloadViewModel.createResultItemFromDownload(downloadItem) return ResultItem(
0,
downloadItem.url,
downloadItem.title,
downloadItem.author,
downloadItem.duration,
downloadItem.thumb,
downloadItem.website,
downloadItem.playlistTitle,
downloadItem.allFormats,
"",
arrayListOf(),
downloadItem.playlistURL,
downloadItem.playlistIndex,
System.currentTimeMillis()
)
} }
fun createResultItemFromHistory(downloadItem: HistoryItem) : ResultItem { fun createResultItemFromHistory(downloadItem: HistoryItem) : ResultItem {
@ -187,7 +334,22 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun createEmptyResultItem(url: String) : ResultItem { fun createEmptyResultItem(url: String) : ResultItem {
return sharedDownloadViewModel.createEmptyResultItem(url) return ResultItem(
0,
url,
"",
"",
"",
"",
"",
"",
arrayListOf(),
"",
arrayListOf(),
"",
null,
System.currentTimeMillis()
)
} }
fun switchDownloadType(list: List<DownloadItem>, type: Type) : List<DownloadItem>{ fun switchDownloadType(list: List<DownloadItem>, type: Type) : List<DownloadItem>{
@ -300,73 +462,112 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
return sharedDownloadViewModel.getPreferredAudioRequirements()
}
//requirement and importance
@SuppressLint("RestrictedApi")
fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
return sharedDownloadViewModel.getPreferredVideoRequirements()
}
fun getFormat(formats: List<Format>, type: Type) : Format { fun getFormat(formats: List<Format>, type: Type) : Format {
return sharedDownloadViewModel.getFormat(formats, type) when(type) {
Type.audio -> {
return cloneFormat (
try {
val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
FormatSorter(application).sortAudioFormats(theFormats).first()
}catch (e: Exception){
infoUtil.getGenericAudioFormats(resources).first()
}
)
}
Type.video -> {
return cloneFormat(
try {
val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty {
infoUtil.getGenericVideoFormats(resources).sortedByDescending { it.filesize }
}
FormatSorter(application).sortVideoFormats(theFormats).first()
}catch (e: Exception){
infoUtil.getGenericVideoFormats(resources).first()
}
)
}
else -> {
val lastUsedCommandTemplate = sharedPreferences.getString("lastCommandTemplateUsed", "")!!
val c = if (lastUsedCommandTemplate.isBlank()){
commandTemplateDao.getFirst() ?: CommandTemplate(0,"","", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
}else{
commandTemplateDao.getTemplateByContent(lastUsedCommandTemplate) ?: CommandTemplate(0, "", lastUsedCommandTemplate, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
}
return generateCommandFormat(c)
}
}
}
private fun cloneFormat(item: Format) : Format {
val string = Gson().toJson(item, Format::class.java)
return Gson().fromJson(string, Format::class.java)
} }
fun getPreferredAudioFormats(formats: List<Format>) : ArrayList<String>{ fun getPreferredAudioFormats(formats: List<Format>) : ArrayList<String>{
return sharedDownloadViewModel.getPreferredAudioFormats(formats) val preferredAudioFormats = arrayListOf<String>()
val audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
for (f in formats.sortedBy { it.format_id }){
val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
if (fId != null) {
if (fId.split("+").all { formats.map { f-> f.format_id }.contains(it) }){
preferredAudioFormats.addAll(fId.split("+"))
break
}
}
}
if (preferredAudioFormats.isEmpty()){
val audioF = getFormat(formats, Type.audio)
if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){
preferredAudioFormats.add(audioF.format_id)
}
}
return preferredAudioFormats
} }
fun generateCommandFormat(c: CommandTemplate) : Format { fun generateCommandFormat(c: CommandTemplate) : Format {
return sharedDownloadViewModel.generateCommandFormat(c) return Format(
c.title,
c.id.toString(),
"",
"",
"",
0,
c.content.replace("\n", " ")
)
} }
data class ProcessingItemsJob(
var job: Job? = null,
var originItemType: String,
var originItemIDs: List<Long>,
var processingDownloadItemIDs: MutableList<Long> = mutableListOf()
)
private fun updateProcessingJobData(item: ProcessingItemsJob?) {
processingItemsFlow = item
}
fun turnDownloadItemsToProcessingDownloads(itemIDs: List<Long>) = viewModelScope.launch(Dispatchers.IO){ fun turnDownloadItemsToProcessingDownloads(itemIDs: List<Long>) = viewModelScope.launch(Dispatchers.IO){
updateProcessingJobData(ProcessingItemsJob(null, DownloadItem::class.java.toString(), itemIDs))
val job = viewModelScope.launch(Dispatchers.IO) { val job = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessing() repository.deleteProcessing()
processingItems.emit(true) processingItems.emit(true)
try { try {
itemIDs.forEachIndexed { idx, it -> itemIDs.forEachIndexed { idx, it ->
val item = repository.getItemByID(it) val item = repository.getItemByID(it)
if (processingItemsFlow?.job?.isCancelled == true) throw CancellationException() if (processingItemsJob?.isCancelled == true) throw CancellationException()
item.id = 0 item.id = 0
item.status = DownloadRepository.Status.Processing.toString() item.status = DownloadRepository.Status.Processing.toString()
processingItemsFlow?.apply { repository.insert(item)
val id = repository.insert(item)
processingDownloadItemIDs.add(id)
updateProcessingJobData(this)
}
} }
processingItems.emit(false) processingItems.emit(false)
} catch (e: Exception) { } catch (e: Exception) {
deleteProcessing() deleteProcessing()
updateProcessingJobData(null)
processingItems.emit(false) processingItems.emit(false)
} }
} }
updateProcessingJobData(ProcessingItemsJob(job, DownloadItem::class.java.toString(), itemIDs)) processingItemsJob = job
} }
fun turnResultItemsToProcessingDownloads(itemIDs: List<Long>, downloadNow: Boolean = false) = viewModelScope.launch(Dispatchers.IO) { fun turnResultItemsToProcessingDownloads(itemIDs: List<Long>, downloadNow: Boolean = false) = viewModelScope.launch(Dispatchers.IO) {
updateProcessingJobData(ProcessingItemsJob(null, ResultItem::class.java.toString(), itemIDs))
val job = viewModelScope.launch(Dispatchers.IO) { val job = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessing() repository.deleteProcessing()
processingItems.emit(true) processingItems.emit(true)
try { try {
val toInsert = mutableListOf<DownloadItem>()
itemIDs.forEach { id -> itemIDs.forEach { id ->
val item = resultRepository.getItemByID(id) ?: return@forEach val item = resultRepository.getItemByID(id) ?: return@forEach
val preferredType = getDownloadType(url = item.url).toString() val preferredType = getDownloadType(url = item.url).toString()
@ -375,7 +576,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
)) ))
downloadItem.status = DownloadRepository.Status.Processing.toString() downloadItem.status = DownloadRepository.Status.Processing.toString()
if (processingItemsFlow?.job?.isCancelled == true) { if (processingItemsJob?.isCancelled == true) {
throw CancellationException() throw CancellationException()
} }
@ -383,21 +584,20 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
downloadItem.status = DownloadRepository.Status.Queued.toString() downloadItem.status = DownloadRepository.Status.Queued.toString()
queueDownloads(listOf(downloadItem)) queueDownloads(listOf(downloadItem))
}else{ }else{
processingItemsFlow?.apply { toInsert.add(downloadItem)
processingDownloadItemIDs.add(repository.insert(downloadItem)) //repository.insert(downloadItem)
updateProcessingJobData(this)
}
} }
} }
repository.insertAll(toInsert)
processingItems.emit(false) processingItems.emit(false)
}catch (e: Exception) { }catch (e: Exception) {
deleteProcessing() deleteProcessing()
updateProcessingJobData(null)
processingItems.emit(false) processingItems.emit(false)
} }
} }
updateProcessingJobData(ProcessingItemsJob(job, ResultItem::class.java.toString(), itemIDs)) processingItemsJob = job
} }
fun insert(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){ fun insert(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){
@ -423,10 +623,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun deleteScheduled() = viewModelScope.launch(Dispatchers.IO) { fun deleteScheduled() = viewModelScope.launch(Dispatchers.IO) {
val scheduledIds = repository.getScheduledDownloadIDs()
scheduledIds.forEach {
WorkManager.getInstance(application).cancelAllWorkByTag(it.toString())
}
repository.deleteScheduled() repository.deleteScheduled()
} }
@ -451,7 +647,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) { fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) {
processingItemsFlow?.apply { this.job?.cancel(CancellationException()) } processingItemsJob?.apply { cancel(CancellationException()) }
repository.cancelActiveQueued() repository.cancelActiveQueued()
} }
@ -574,9 +770,193 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
queueDownloads(processingItems) queueDownloads(processingItems)
} }
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : List<SharedDownloadViewModel.AlreadyExistsIDs> { suspend fun queueDownloads(items: List<DownloadItem>, ignoreDuplicates : Boolean = false) {
val ids = sharedDownloadViewModel.queueDownloads(items, ign) val context = App.instance
return ids val alarmScheduler = AlarmScheduler(context)
val queuedItems = mutableListOf<DownloadItem>()
//download id, history item id
//history item id if the existing item is already downloaded
//if history id is empty, it just found an existing item in the queue/active list
val existingItemIDs = mutableListOf<AlreadyExistsIDs>()
val downloadArchive = runCatching {
File(FileUtil.getDownloadArchivePath(context)).useLines { it.toList() }
}
.getOrElse { listOf() }
.map { it.split(" ")[1] }
val checkDuplicate = sharedPreferences.getString("prevent_duplicate_downloads", "")!!
val activeAndQueuedDownloads = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloads()
}
items.forEach {
if (it.status != DownloadRepository.Status.Scheduled.toString()) {
it.status = DownloadRepository.Status.Queued.toString()
}
//CHECK DUPLICATES
var alreadyExists = false
if (checkDuplicate.isNotEmpty() && !ignoreDuplicates){
when(checkDuplicate){
"download_archive" -> {
if (downloadArchive.any { d -> it.url.contains(d) }){
alreadyExists = true
if (it.id == 0L) {
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
it.id = id
}
existingItemIDs.add(
AlreadyExistsIDs(
it.id,
null
)
)
}
}
"url_type" -> {
val existingDownload = activeAndQueuedDownloads.firstOrNull { a -> a.type == it.type && a.url == it.url }
if (existingDownload != null){
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
it.id = id
alreadyExists = true
existingItemIDs.add(
AlreadyExistsIDs(
it.id,
null
)
)
}else{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
val existingHistoryItem = history.firstOrNull {
h -> h.type == it.type
}
if (existingHistoryItem != null){
alreadyExists = true
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
existingItemIDs.add(
AlreadyExistsIDs(
id,
existingHistoryItem.id
)
)
}
}
}
"config" -> {
val currentCommand = infoUtil.buildYoutubeDLRequest(it)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
}
if (existingDownload != null){
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
alreadyExists = true
existingItemIDs.add(AlreadyExistsIDs(id, null))
}else{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
val existingHistoryItem = history.firstOrNull {
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
}
if (existingHistoryItem != null){
alreadyExists = true
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
existingItemIDs.add(
AlreadyExistsIDs(
id,
existingHistoryItem.id
)
)
}
}
}
}
}
if (!alreadyExists){
queuedItems.add(it)
}
}
repository.updateAll(queuedItems)
//if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
if (alarmScheduler.canSchedule()){
alarmScheduler.schedule()
}else{
sharedPreferences.edit().putBoolean("use_scheduler", false).apply()
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, context.getString(R.string.enable_alarm_permission), Toast.LENGTH_LONG).show()
}
}
}else{
if (!sharedPreferences.getBoolean("paused_downloads", false)) {
repository.startDownloadWorker(queuedItems, context)
}
if(!useScheduler){
CoroutineScope(Dispatchers.IO).launch {
queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
kotlin.runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}else{
CoroutineScope(Dispatchers.IO).launch {
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
kotlin.runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}
}
if (existingItemIDs.isNotEmpty()){
alreadyExistsUiState.value = existingItemIDs.toList()
}
} }
fun getQueuedCollectedFileSize() : Long { fun getQueuedCollectedFileSize() : Long {
@ -599,17 +979,25 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao.updateProcessingtoSavedStatus() dao.updateProcessingtoSavedStatus()
} }
suspend fun updateProcessingFormat(selectedFormats: List<FormatTuple>): List<Long> {
fun updateAllProcessingFormats(formatTuples : List<MultipleItemFormatTuple>) = viewModelScope.launch(Dispatchers.IO) {
val items = repository.getProcessingDownloads() val items = repository.getProcessingDownloads()
items.forEachIndexed { index, i -> items.forEach {
selectedFormats[index].format?.apply { val ft = formatTuples.first { ft -> ft.url == it.url }.formatTuple
i.format = this ft.format?.apply {
it.format = this
} }
if (i.type == Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
repository.update(i) if (it.type == Type.video) {
ft.audioFormats?.map { a -> a.format_id }?.let { list ->
it.videoPreferences.audioFormatIDs.clear()
it.videoPreferences.audioFormatIDs.addAll(list)
}
}
repository.update(it)
} }
return items.map { itm -> itm.format.filesize }
} }
suspend fun updateProcessingCommandFormat(format: Format){ suspend fun updateProcessingCommandFormat(format: Format){
@ -624,37 +1012,50 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao.updateProcessingDownloadPath(path) dao.updateProcessingDownloadPath(path)
} }
fun getProcessingDownloadsCount() : Int {
return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Processing.toString()))
}
fun getProcessingDownloads() : List<DownloadItem> { fun getProcessingDownloads() : List<DownloadItem> {
return repository.getProcessingDownloads() return repository.getProcessingDownloads()
} }
fun updateDownloadItemFormats(id: Long, list: List<Format>) = viewModelScope.launch(Dispatchers.IO) {
val item = repository.getItemByID(id)
item.allFormats.clear()
item.allFormats.addAll(list)
item.format = getFormat(list, item.type)
suspend fun updateProcessingAllFormats(formatCollection: List<List<Format>>) { runCatching {
val items = repository.getProcessingDownloads() resultRepository.getAllByURL(item.url).forEach {
items.forEachIndexed { index, i -> it.formats.clear()
i.allFormats.clear() it.formats.addAll(list)
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) { resultRepository.update(it)
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)
}
}
repository.update(i)
} }
} }
fun updateProcessingFormatByUrl(url: String, list: List<Format>) = viewModelScope.launch(Dispatchers.IO) {
val items = repository.getProcessingDownloadsByUrl(url)
items.forEach { item ->
item.allFormats.clear()
item.allFormats.addAll(list)
item.format = getFormat(list, item.type)
repository.update(item)
}
kotlin.runCatching {
resultRepository.getAllByURL(url).forEach {
it.formats.clear()
it.formats.addAll(list)
resultRepository.update(it)
}
}
}
fun removeUnavailableDownloadAndResultByURL(url: String) = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessingByUrl(url)
resultRepository.deleteByUrl(url)
}
suspend fun continueUpdatingFormatsOnBackground(){ suspend fun continueUpdatingFormatsOnBackground(){
val ids = dao.getProcessingDownloadsList().map { it.id } val ids = repository.getProcessingDownloads().map { it.id }
dao.updateProcessingtoSavedStatus() dao.updateProcessingtoSavedStatus()
val id = System.currentTimeMillis().toInt() val id = System.currentTimeMillis().toInt()
@ -685,12 +1086,21 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
} }
suspend fun updateProcessingDownloadTime(time: Long) { suspend fun updateProcessingDownloadTimeAndQueueScheduled(time: Long) {
repository.updateProcessingDownloadTime(time) val processing = repository.getProcessingDownloads()
processing.forEach {
it.downloadStartTime = time
it.status = DownloadRepository.Status.Scheduled.toString()
}
queueDownloads(processing)
} }
fun checkIfAllProcessingItemsHaveSameType() : Pair<Boolean, Type> { fun checkIfAllProcessingItemsHaveSameType() : Pair<Boolean, Type> {
val types = dao.getProcessingDownloadTypes() val types = dao.getProcessingDownloadTypes()
if (types.isEmpty()) {
return Pair(false, Type.command)
}
return Pair(types.size == 1, Type.valueOf(types.first())) return Pair(types.size == 1, Type.valueOf(types.first()))
} }

View file

@ -1,665 +0,0 @@
package com.deniscerri.ytdl.database.viewmodel
import android.annotation.SuppressLint
import android.content.Context
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.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.dao.CommandTemplateDao
import com.deniscerri.ytdl.database.dao.DownloadDao
import com.deniscerri.ytdl.database.models.AudioPreferences
import com.deniscerri.ytdl.database.models.CommandTemplate
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.models.VideoPreferences
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatSorter
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
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
class SharedDownloadViewModel(private val context: Context) {
private val dbManager: DBManager = DBManager.getInstance(context)
val repository : DownloadRepository
private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao
private val infoUtil : InfoUtil
private var bestVideoFormat : Format
private var bestAudioFormat : Format
private var defaultVideoFormats : MutableList<Format>
private val videoQualityPreference: String
private val formatIDPreference: List<String>
private val audioFormatIDPreference: List<String>
private val resources : Resources
private var extraCommandsForAudio: String = ""
private var extraCommandsForVideo: String = ""
private var audioContainer: String?
private var videoContainer: String?
private var videoCodec: String?
private var audioCodec: String?
private val dao: DownloadDao
private val historyRepository: HistoryRepository
private val resultRepository: ResultRepository
@Parcelize
data class AlreadyExistsIDs(
var downloadItemID: Long,
var historyItemID : Long?
) : Parcelable
val alreadyExistsUiState: MutableStateFlow<List<AlreadyExistsIDs>> = MutableStateFlow(
mutableListOf()
)
private val urlsForAudioType = listOf(
"music",
"audio",
"soundcloud"
)
init {
dao = dbManager.downloadDao
repository = DownloadRepository(dao)
historyRepository = HistoryRepository(dbManager.historyDao)
resultRepository = ResultRepository(dbManager.resultDao, context)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
commandTemplateDao = DBManager.getInstance(context).commandTemplateDao
infoUtil = InfoUtil(context)
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
if (sharedPreferences.getBoolean("use_extra_commands", false)){
extraCommandsForAudio = commandTemplateDao.getAllTemplatesAsExtraCommandsForAudio().joinToString(" ")
extraCommandsForVideo = commandTemplateDao.getAllTemplatesAsExtraCommandsForVideo().joinToString(" ")
}
}
videoQualityPreference = sharedPreferences.getString("video_quality", "best").toString()
formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() }
audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
val confTmp = Configuration(context.resources.configuration)
confTmp.setLocale(Locale(sharedPreferences.getString("app_language", "en")!!))
val metrics = DisplayMetrics()
resources = Resources(context.assets, metrics, confTmp)
videoContainer = sharedPreferences.getString("video_format", "Default")
defaultVideoFormats = infoUtil.getGenericVideoFormats(resources)
bestVideoFormat = defaultVideoFormats.first()
audioContainer = sharedPreferences.getString("audio_format", "mp3")
bestAudioFormat = if (audioFormatIDPreference.isEmpty()){
infoUtil.getGenericAudioFormats(resources).first()
}else{
Format(
audioFormatIDPreference.first().split("+").first(),
audioContainer!!,
"",
"",
"",
0,
audioFormatIDPreference.first().split("+").first()
)
}
videoCodec = sharedPreferences.getString("video_codec", "")
audioCodec = sharedPreferences.getString("audio_codec", "")
}
fun getDownloadType(t: Type? = null, url: String) : Type {
var type = t
if (type == null){
val preferredDownloadType = sharedPreferences.getString("preferred_download_type", Type.auto.toString())
type = if (sharedPreferences.getBoolean("remember_download_type", false)){
Type.valueOf(sharedPreferences.getString("last_used_download_type",
preferredDownloadType)!!)
}else{
Type.valueOf(preferredDownloadType!!)
}
}
return when(type){
Type.auto -> {
if (urlsForAudioType.any { url.contains(it) }){
Type.audio
}else{
Type.video
}
}
else -> type
}
}
fun createDownloadItemFromResult(result: ResultItem?, url: String = "", givenType: Type) : DownloadItem {
val resultItem = result ?: createEmptyResultItem(url)
val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
val saveAutoSubs = sharedPreferences.getBoolean("write_auto_subtitles", false)
val addChapters = sharedPreferences.getBoolean("add_chapters", false)
val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
val cropThumb = sharedPreferences.getBoolean("crop_thumbnail", false)
var type = getDownloadType(givenType, resultItem.url)
if(type == Type.command && commandTemplateDao.getTotalNumber() == 0) type = Type.video
val customFileNameTemplate = when(type) {
Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader).30B - %(title).170B")
Type.video -> sharedPreferences.getString("file_name_template", "%(uploader).30B - %(title).170B")
else -> ""
}
val downloadPath = when(type){
Type.audio -> sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())
Type.video -> sharedPreferences.getString("video_path", FileUtil.getDefaultVideoPath())
else -> sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
}
val container = when(type){
Type.audio -> sharedPreferences.getString("audio_format", "")
else -> sharedPreferences.getString("video_format", "")
}
val sponsorblock = sharedPreferences.getStringSet("sponsorblock_filters", emptySet())
val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!))
val preferredAudioFormats = getPreferredAudioFormats(resultItem.formats)
val videoPreferences = VideoPreferences(
embedSubs,
addChapters, false,
ArrayList(sponsorblock),
saveSubs,
saveAutoSubs,
audioFormatIDs = preferredAudioFormats
)
val extraCommands = when(type){
Type.audio -> extraCommandsForAudio
Type.video -> extraCommandsForVideo
else -> ""
}
return DownloadItem(0,
resultItem.url,
resultItem.title,
resultItem.author,
resultItem.thumb,
resultItem.duration,
type,
getFormat(resultItem.formats, type),
container!!,
"",
resultItem.formats,
downloadPath!!, resultItem.website,
"",
if (resultItem.playlistTitle == resultRepository.YTDLNIS_SEARCH) "" else resultItem.playlistTitle,
audioPreferences,
videoPreferences,
extraCommands,
customFileNameTemplate!!,
saveThumb,
DownloadRepository.Status.Queued.toString(),
0,
null,
playlistURL = resultItem.playlistURL,
playlistIndex = resultItem.playlistIndex,
incognito = sharedPreferences.getBoolean("incognito", false)
)
}
fun createResultItemFromDownload(downloadItem: DownloadItem) : ResultItem {
return ResultItem(
0,
downloadItem.url,
downloadItem.title,
downloadItem.author,
downloadItem.duration,
downloadItem.thumb,
downloadItem.website,
downloadItem.playlistTitle,
downloadItem.allFormats,
"",
arrayListOf(),
downloadItem.playlistURL,
downloadItem.playlistIndex,
System.currentTimeMillis()
)
}
fun createEmptyResultItem(url: String) : ResultItem {
return ResultItem(
0,
url,
"",
"",
"",
"",
"",
"",
arrayListOf(),
"",
arrayListOf(),
"",
null,
System.currentTimeMillis()
)
}
fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
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 ->
val 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 }
}
}
}
return requirements
}
//requirement and importance
@SuppressLint("RestrictedApi")
fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
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 ->
var 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
}
}
}
}
return requirements
}
fun getFormat(formats: List<Format>, type: Type) : Format {
when(type) {
Type.audio -> {
return cloneFormat (
try {
val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
FormatSorter(context).sortAudioFormats(theFormats).first()
//
// val requirements = getPreferredAudioRequirements()
// theFormats.maxByOrNull { f -> requirements.sumOf{ req -> req(f)} } ?: throw Exception()
}catch (e: Exception){
bestAudioFormat
}
)
}
Type.video -> {
return cloneFormat(
try {
val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty {
defaultVideoFormats.sortedByDescending { it.filesize }
}
FormatSorter(context).sortVideoFormats(theFormats).first()
//
// when (videoQualityPreference) {
// "worst" -> {
// theFormats.last()
// }
// else /*best*/ -> {
// val requirements = getPreferredVideoRequirements()
// theFormats.run {
// if (sharedPreferences.getBoolean("prefer_smaller_formats", false)){
// sortedBy { it.filesize }.maxByOrNull { f -> requirements.sumOf { req -> req(f) } } ?: throw Exception()
// }else{
// sortedByDescending { it.filesize }.maxByOrNull { f ->
// val summ = requirements.sumOf { req -> req(f) }
// summ
// } ?: throw Exception()
// }
// }
// }
// }
}catch (e: Exception){
bestVideoFormat
}
)
}
else -> {
val lastUsedCommandTemplate = sharedPreferences.getString("lastCommandTemplateUsed", "")!!
val c = if (lastUsedCommandTemplate.isBlank()){
commandTemplateDao.getFirst() ?: CommandTemplate(0,"","", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
}else{
commandTemplateDao.getTemplateByContent(lastUsedCommandTemplate) ?: CommandTemplate(0, "", lastUsedCommandTemplate, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
}
return generateCommandFormat(c)
}
}
}
fun getPreferredAudioFormats(formats: List<Format>) : ArrayList<String>{
val preferredAudioFormats = arrayListOf<String>()
for (f in formats.sortedBy { it.format_id }){
val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
if (fId != null) {
if (fId.split("+").all { formats.map { f-> f.format_id }.contains(it) }){
preferredAudioFormats.addAll(fId.split("+"))
break
}
}
}
if (preferredAudioFormats.isEmpty()){
val audioF = getFormat(formats, Type.audio)
if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){
preferredAudioFormats.add(audioF.format_id)
}
}
return preferredAudioFormats
}
fun generateCommandFormat(c: CommandTemplate) : Format {
return Format(
c.title,
c.id.toString(),
"",
"",
"",
0,
c.content.replace("\n", " ")
)
}
private fun cloneFormat(item: Format) : Format {
val string = Gson().toJson(item, Format::class.java)
return Gson().fromJson(string, Format::class.java)
}
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : List<AlreadyExistsIDs> {
val context = App.instance
val alarmScheduler = AlarmScheduler(context)
val queuedItems = mutableListOf<DownloadItem>()
//download id, history item id
//history item id if the existing item is already downloaded
val existingItemIDs = mutableListOf<AlreadyExistsIDs>()
if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){
items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" }
}
val downloadArchive = runCatching { File(FileUtil.getDownloadArchivePath(context)).useLines { it.toList() } }.getOrElse { listOf() }
.map { it.split(" ")[1] }
items.forEach {
if (it.status != DownloadRepository.Status.Scheduled.toString())
it.status = DownloadRepository.Status.Queued.toString()
var alreadyExists = false
val checkDuplicate = sharedPreferences.getString("prevent_duplicate_downloads", "")!!
if (checkDuplicate.isNotEmpty() && !ign){
when(checkDuplicate){
"download_archive" -> {
if (downloadArchive.any { d -> it.url.contains(d) }){
alreadyExists = true
if (it.id == 0L) {
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
it.id = id
}
existingItemIDs.add(AlreadyExistsIDs(it.id, null))
}
}
"url_type" -> {
val activeAndQueuedDownloads = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloads()
}
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
}
if (existingDownload != null){
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
it.id = id
alreadyExists = true
existingItemIDs.add(AlreadyExistsIDs(it.id, null))
}else{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
val existingHistoryItem = history.firstOrNull {
h -> h.type == it.type
}
if (existingHistoryItem != null){
alreadyExists = true
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
existingItemIDs.add(AlreadyExistsIDs(id, existingHistoryItem.id))
}
}
}
"config" -> {
val currentCommand = infoUtil.buildYoutubeDLRequest(it)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
val activeAndQueuedDownloads = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloads()
}
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
}
if (existingDownload != null){
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
alreadyExists = true
existingItemIDs.add(AlreadyExistsIDs(id, null))
}else{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
val existingHistoryItem = history.firstOrNull {
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
}
if (existingHistoryItem != null){
alreadyExists = true
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
existingItemIDs.add(AlreadyExistsIDs(id, existingHistoryItem.id))
}
}
}
}
}
if (!alreadyExists){
if (it.id == 0L){
val id = runBlocking {
repository.insert(it)
}
it.id = id
}else if (listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.Scheduled).toListString().contains(it.status)){
withContext(Dispatchers.IO){
repository.update(it)
}
}
queuedItems.add(it)
}
}
if (existingItemIDs.isNotEmpty()){
alreadyExistsUiState.value = existingItemIDs.toList()
}
//if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
if (alarmScheduler.canSchedule()){
alarmScheduler.schedule()
}else{
sharedPreferences.edit().putBoolean("use_scheduler", false).apply()
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, context.getString(R.string.enable_alarm_permission), Toast.LENGTH_LONG).show()
}
}
}else{
if (queuedItems.isNotEmpty()){
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 {
CoroutineScope(Dispatchers.IO).launch {
runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}else{
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
CoroutineScope(Dispatchers.IO).launch {
runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}
}
}
return existingItemIDs
}
}

View file

@ -11,13 +11,14 @@ import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.paging.PagingDataAdapter
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItemSimple
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.Extensions.loadThumbnail import com.deniscerri.ytdl.util.Extensions.loadThumbnail
import com.deniscerri.ytdl.util.Extensions.popup import com.deniscerri.ytdl.util.Extensions.popup
@ -25,7 +26,8 @@ import com.deniscerri.ytdl.util.FileUtil
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import java.util.Locale import java.util.Locale
class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, ConfigureMultipleDownloadsAdapter.ViewHolder>(AsyncDifferConfig.Builder( class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItemSimple?, ConfigureMultipleDownloadsAdapter.ViewHolder>(
AsyncDifferConfig.Builder(
DIFF_CALLBACK DIFF_CALLBACK
).build()) { ).build()) {
private val onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener
@ -155,12 +157,12 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
} }
companion object { companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<DownloadItem> = object : DiffUtil.ItemCallback<DownloadItem>() { private val DIFF_CALLBACK: DiffUtil.ItemCallback<DownloadItemSimple> = object : DiffUtil.ItemCallback<DownloadItemSimple>() {
override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean { override fun areItemsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean {
return oldItem.url == newItem.url return oldItem.url == newItem.url
} }
override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean { override fun areContentsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean {
return oldItem.title == newItem.title && return oldItem.title == newItem.title &&
oldItem.author == newItem.author && oldItem.author == newItem.author &&
oldItem.type == newItem.type && oldItem.type == newItem.type &&

View file

@ -41,6 +41,9 @@ class NavBarOptionsAdapter(
val noHome = listOf(R.id.terminalActivity) val noHome = listOf(R.id.terminalActivity)
holder.binding.apply { holder.binding.apply {
title.text = item.title title.text = item.title
title.contentDescription = item.title
checkbox.isChecked = item.isVisible || essential.contains(item.itemId) checkbox.isChecked = item.isVisible || essential.contains(item.itemId)
checkbox.isEnabled = !essential.contains(item.itemId) checkbox.isEnabled = !essential.contains(item.itemId)
home.setImageResource( home.setImageResource(
@ -52,11 +55,8 @@ class NavBarOptionsAdapter(
if (!checkbox.isChecked || selectedHomeTabId == item.itemId) { if (!checkbox.isChecked || selectedHomeTabId == item.itemId) {
return@setOnClickListener return@setOnClickListener
} }
val oldSelection = items.indexOfFirst { it.itemId == selectedHomeTabId }
selectedHomeTabId = item.itemId selectedHomeTabId = item.itemId
listOf(position, oldSelection).forEach { notifyDataSetChanged()
notifyItemChanged(it)
}
} }
checkbox.setOnClickListener { checkbox.setOnClickListener {
item.isVisible = checkbox.isChecked item.isVisible = checkbox.isChecked

View file

@ -113,26 +113,28 @@ class ConfigureDownloadBottomSheetDialog(private val currentDownloadItem: Downlo
viewPager2.adapter = fragmentAdapter viewPager2.adapter = fragmentAdapter
viewPager2.isSaveFromParentEnabled = false viewPager2.isSaveFromParentEnabled = false
when(currentDownloadItem.type) { view.post {
Type.audio -> { when(currentDownloadItem.type) {
tabLayout.selectTab(tabLayout.getTabAt(0)) Type.audio -> {
viewPager2.setCurrentItem(0, false) tabLayout.selectTab(tabLayout.getTabAt(0))
}
Type.video -> {
if (isAudioOnly){
tabLayout.getTabAt(0)!!.select()
viewPager2.setCurrentItem(0, false) viewPager2.setCurrentItem(0, false)
Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show()
}else{
tabLayout.getTabAt(1)!!.select()
viewPager2.setCurrentItem(1, false)
} }
} Type.video -> {
else -> { if (isAudioOnly){
tabLayout.selectTab(tabLayout.getTabAt(2)) tabLayout.getTabAt(0)!!.select()
viewPager2.postDelayed( { viewPager2.setCurrentItem(0, false)
viewPager2.setCurrentItem(2, false) Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show()
}, 200) }else{
tabLayout.getTabAt(1)!!.select()
viewPager2.setCurrentItem(1, false)
}
}
else -> {
tabLayout.selectTab(tabLayout.getTabAt(2))
viewPager2.postDelayed( {
viewPager2.setCurrentItem(2, false)
}, 200)
}
} }
} }

View file

@ -226,26 +226,30 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
val chosenFormat = downloadItem.format val chosenFormat = downloadItem.format
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null) UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null)
val listener = object : OnFormatClickListener { val listener = object : OnFormatClickListener {
override fun onFormatClick(item: List<FormatTuple>) { override fun onFormatClick(formatTuple: FormatTuple) {
item.first().format?.apply { formatTuple.format?.apply {
downloadItem.format = this downloadItem.format = this
UiUtil.populateFormatCard(requireContext(), formatCard, this, null) UiUtil.populateFormatCard(requireContext(), formatCard, this, null)
} }
} }
override fun onFormatsUpdated(allFormats: List<List<Format>>) { override fun onFormatsUpdated(allFormats: List<Format>) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
resultItem?.apply { resultItem?.apply {
this.formats.removeAll(formats.toSet()) this.formats.removeAll(formats.toSet())
this.formats.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) }) this.formats.addAll(allFormats.filter { !genericAudioFormats.contains(it) })
resultViewModel.update(this) resultViewModel.update(this)
kotlin.runCatching { kotlin.runCatching {
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f1.updateUI(this) f1.updateUI(this)
} }
} }
currentDownloadItem?.apply {
downloadViewModel.updateDownloadItemFormats(this.id, allFormats.filter { !genericAudioFormats.contains(it) })
}
} }
formats = allFormats.first().filter { !genericAudioFormats.contains(it) }.toMutableList() formats = allFormats.filter { !genericAudioFormats.contains(it) }.toMutableList()
formats.removeAll(genericAudioFormats) formats.removeAll(genericAudioFormats)
val preferredFormat = downloadViewModel.getFormat(formats, Type.audio) val preferredFormat = downloadViewModel.getFormat(formats, Type.audio)
downloadItem.format = preferredFormat downloadItem.format = preferredFormat
@ -255,7 +259,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
} }
formatCard.setOnClickListener{ formatCard.setOnClickListener{
if (parentFragmentManager.findFragmentByTag("formatSheet") == null){ if (parentFragmentManager.findFragmentByTag("formatSheet") == null){
val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listOf(formats.ifEmpty { genericAudioFormats }), listener) val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listener)
bottomSheet.show(parentFragmentManager, "formatSheet") bottomSheet.show(parentFragmentManager, "formatSheet")
} }
} }

View file

@ -184,7 +184,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
//check if the item has formats and its audio-only //check if the item has formats and its audio-only
val formats = result.formats val formats = result.formats
val isAudioOnly = formats.isNotEmpty() && formats.none { !it.format_note.contains("audio") } var isAudioOnly = formats.isNotEmpty() && formats.none { !it.format_note.contains("audio") }
if (isAudioOnly){ if (isAudioOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
@ -216,26 +216,28 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
viewPager2.adapter = fragmentAdapter viewPager2.adapter = fragmentAdapter
viewPager2.isSaveFromParentEnabled = false viewPager2.isSaveFromParentEnabled = false
when(type) { view.post {
Type.audio -> { when(type) {
tabLayout.getTabAt(0)!!.select() Type.audio -> {
viewPager2.setCurrentItem(0, false)
}
Type.video -> {
if (isAudioOnly){
tabLayout.getTabAt(0)!!.select() tabLayout.getTabAt(0)!!.select()
viewPager2.setCurrentItem(0, false) viewPager2.setCurrentItem(0, false)
Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show()
}else{
tabLayout.getTabAt(1)!!.select()
viewPager2.setCurrentItem(1, false)
} }
} Type.video -> {
else -> { if (isAudioOnly){
tabLayout.getTabAt(2)!!.select() tabLayout.getTabAt(0)!!.select()
viewPager2.postDelayed( { viewPager2.setCurrentItem(0, false)
viewPager2.setCurrentItem(2, false) Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show()
}, 200) }else{
tabLayout.getTabAt(1)!!.select()
viewPager2.setCurrentItem(1, false)
}
}
else -> {
tabLayout.getTabAt(2)!!.select()
viewPager2.postDelayed( {
viewPager2.setCurrentItem(2, false)
}, 200)
}
} }
} }
@ -592,6 +594,15 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
resultViewModel.updateFormatsResultData.collectLatest { formats -> resultViewModel.updateFormatsResultData.collectLatest { formats ->
if (formats == null) return@collectLatest if (formats == null) return@collectLatest
kotlin.runCatching { kotlin.runCatching {
isAudioOnly = formats.isNotEmpty() && formats.none { !it.format_note.contains("audio") }
if (isAudioOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show()
tabLayout.getTabAt(0)!!.select()
viewPager2.setCurrentItem(0, false)
}
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
runCatching { runCatching {

View file

@ -201,9 +201,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
toggleLoading(true) toggleLoading(true)
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.updateProcessingDownloadTime(cal.timeInMillis)
downloadViewModel.deleteAllWithID(currentDownloadIDs) downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.queueProcessingDownloads() downloadViewModel.updateProcessingDownloadTimeAndQueueScheduled(cal.timeInMillis)
} }
dismiss() dismiss()
} }
@ -231,10 +230,10 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
downloadViewModel.deleteAllWithID(currentDownloadIDs) downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.moveProcessingToSavedCategory() downloadViewModel.moveProcessingToSavedCategory()
} }
getProcessingItemsData()?.apply {
this.job?.cancel(CancellationException()) downloadViewModel.processingItemsJob?.cancel(CancellationException())
this.job = null downloadViewModel.processingItemsJob = null
}
dismiss() dismiss()
} }
} }
@ -242,29 +241,27 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
true true
} }
val formatListener = object : OnFormatClickListener { val formatListener = object : OnMultipleFormatClickListener {
override fun onFormatClick(selectedFormats: List<FormatTuple>) { override fun onFormatClick(formatTuple: List<MultipleItemFormatTuple>) {
CoroutineScope(Dispatchers.IO).launch { downloadViewModel.updateAllProcessingFormats(formatTuple)
downloadViewModel.updateProcessingFormat(selectedFormats)
}
} }
override fun onFormatsUpdated(allFormats: List<List<Format>>) { override fun onFormatUpdated(url: String, formats: List<Format>) {
CoroutineScope(Dispatchers.IO).launch { downloadViewModel.updateProcessingFormatByUrl(url, formats)
downloadViewModel.updateProcessingAllFormats(allFormats)
}
} }
override fun onItemUnavailable(url: String) {
downloadViewModel.removeUnavailableDownloadAndResultByURL(url)
}
override fun onContinueOnBackground() { override fun onContinueOnBackground() {
requireActivity().lifecycleScope.launch { requireActivity().lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.continueUpdatingFormatsOnBackground() downloadViewModel.continueUpdatingFormatsOnBackground()
} }
getProcessingItemsData()?.apply { downloadViewModel.processingItemsJob?.cancel(CancellationException())
this.job?.cancel(CancellationException()) downloadViewModel.processingItemsJob = null
this.job = null
}
dismiss() dismiss()
} }
} }
@ -421,20 +418,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val items = withContext(Dispatchers.IO){ val items = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloads() downloadViewModel.getProcessingDownloads()
} }
val flatFormatCollection = items.map { it.allFormats }.flatten() val bottomSheet = FormatSelectionBottomSheetDialog(items, _multipleFormatsListener = formatListener)
val commonFormats = withContext(Dispatchers.IO){
flatFormatCollection.groupingBy { it.format_id }.eachCount().filter { it.value == items.size }.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }.map { it.value }
}
val formats = if (commonFormats.isNotEmpty() && items.none{it.allFormats.isEmpty()}) {
items.map { it.allFormats }
}else{
when(items.first().type){
DownloadViewModel.Type.audio -> listOf<List<Format>>(infoUtil.getGenericAudioFormats(requireContext().resources))
else -> listOf<List<Format>>(infoUtil.getGenericVideoFormats(requireContext().resources))
}
}
val bottomSheet = FormatSelectionBottomSheetDialog(items, formats, formatListener)
bottomSheet.show(parentFragmentManager, "formatSheet") bottomSheet.show(parentFragmentManager, "formatSheet")
} }
} }
@ -661,10 +645,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading } bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading }
} }
private fun toggleLoadingShimmerTitle(show: Boolean) {
}
private fun updateFileSize(items: List<Long>){ private fun updateFileSize(items: List<Long>){
if (items.all { it > 5L }){ if (items.all { it > 5L }){
val size = FileUtil.convertFileSize(items.sum()) val size = FileUtil.convertFileSize(items.sum())
@ -822,12 +802,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
getProcessingItemsData()?.apply { downloadViewModel.processingItemsJob?.cancel(CancellationException())
if (this.job?.isActive == true){ downloadViewModel.processingItemsJob = null
this.job?.cancel(CancellationException())
downloadViewModel.deleteAllWithID(this.processingDownloadItemIDs)
}
}
downloadViewModel.deleteProcessing() downloadViewModel.deleteProcessing()
} }
} }
@ -912,8 +888,5 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
} }
private fun getProcessingItemsData() : DownloadViewModel.ProcessingItemsJob? {
return downloadViewModel.processingItemsFlow
}
} }

View file

@ -60,7 +60,9 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
private lateinit var saveDir : TextInputLayout private lateinit var saveDir : TextInputLayout
private lateinit var freeSpace : TextView private lateinit var freeSpace : TextView
private lateinit var infoUtil: InfoUtil private lateinit var infoUtil: InfoUtil
private lateinit var genericVideoFormats: MutableList<Format> private lateinit var genericVideoFormats: MutableList<Format>
private lateinit var genericAudioFormats: MutableList<Format>
lateinit var downloadItem: DownloadItem lateinit var downloadItem: DownloadItem
@ -77,6 +79,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
infoUtil = InfoUtil(requireContext()) infoUtil = InfoUtil(requireContext())
genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources) genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources)
genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources)
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
shownFields = preferences.getStringSet("modify_download_card", requireContext().getStringArray(R.array.modify_download_card_values).toSet())!!.toList() shownFields = preferences.getStringSet("modify_download_card", requireContext().getStringArray(R.array.modify_download_card_values).toSet())!!.toList()
return fragmentView return fragmentView
@ -234,34 +237,38 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
val chosenFormat = downloadItem.format val chosenFormat = downloadItem.format
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) }) UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) })
val listener = object : OnFormatClickListener { val listener = object : OnFormatClickListener {
override fun onFormatClick(item: List<FormatTuple>) { override fun onFormatClick(formatTuple: FormatTuple) {
item.first().format?.apply { formatTuple.format?.apply {
downloadItem.format = this downloadItem.format = this
} }
downloadItem.videoPreferences.audioFormatIDs.clear() downloadItem.videoPreferences.audioFormatIDs.clear()
item.first().audioFormats?.map { it.format_id }?.let { formatTuple.audioFormats?.map { it.format_id }?.let {
downloadItem.videoPreferences.audioFormatIDs.addAll(it) downloadItem.videoPreferences.audioFormatIDs.addAll(it)
} }
UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format,
if(downloadItem.videoPreferences.removeAudio) listOf() else item.first().audioFormats if(downloadItem.videoPreferences.removeAudio) listOf() else formatTuple.audioFormats
) )
} }
override fun onFormatsUpdated(allFormats: List<List<Format>>) { override fun onFormatsUpdated(allFormats: List<Format>) {
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
resultItem?.apply { resultItem?.apply {
this.formats.removeAll(formats) this.formats.removeAll(formats)
this.formats.addAll(allFormats.first().filter { !genericVideoFormats.contains(it) }) this.formats.addAll(allFormats.filter { !genericVideoFormats.contains(it) })
resultViewModel.update(this) resultViewModel.update(this)
kotlin.runCatching { kotlin.runCatching {
val f1 = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment val f1 = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f1.updateUI(this) f1.updateUI(this)
} }
} }
currentDownloadItem?.apply {
downloadViewModel.updateDownloadItemFormats(this.id, allFormats.filter { !genericVideoFormats.contains(it) })
}
} }
} }
formats = allFormats.first().filter { !genericVideoFormats.contains(it) }.toMutableList() formats = allFormats.filter { !genericVideoFormats.contains(it) }.toMutableList()
val preferredFormat = downloadViewModel.getFormat(formats, Type.video) val preferredFormat = downloadViewModel.getFormat(formats, Type.video)
val preferredAudioFormats = downloadViewModel.getPreferredAudioFormats(formats) val preferredAudioFormats = downloadViewModel.getPreferredAudioFormats(formats)
downloadItem.format = preferredFormat downloadItem.format = preferredFormat
@ -274,7 +281,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
} }
formatCard.setOnClickListener{ formatCard.setOnClickListener{
if (parentFragmentManager.findFragmentByTag("formatSheet") == null){ if (parentFragmentManager.findFragmentByTag("formatSheet") == null){
val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listOf(formats.ifEmpty { genericVideoFormats }), listener) val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listener)
bottomSheet.show(parentFragmentManager, "formatSheet") bottomSheet.show(parentFragmentManager, "formatSheet")
} }
} }
@ -413,6 +420,10 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
fun updateSelectedAudioFormat(format: Format){ fun updateSelectedAudioFormat(format: Format){
if (genericAudioFormats.contains(format)) {
return
}
downloadItem.videoPreferences.audioFormatIDs.clear() downloadItem.videoPreferences.audioFormatIDs.clear()
downloadItem.videoPreferences.audioFormatIDs.addAll(arrayListOf(format.format_id)) downloadItem.videoPreferences.audioFormatIDs.addAll(arrayListOf(format.format_id))
val formatCard = requireView().findViewById<MaterialCardView>(R.id.format_card_constraintLayout) val formatCard = requireView().findViewById<MaterialCardView>(R.id.format_card_constraintLayout)

View file

@ -18,9 +18,9 @@ import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.AlreadyExistsItem import com.deniscerri.ytdl.database.models.AlreadyExistsItem
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.AlreadyExistsIDs
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.database.viewmodel.SharedDownloadViewModel.AlreadyExistsIDs
import com.deniscerri.ytdl.ui.adapter.AlreadyExistsAdapter import com.deniscerri.ytdl.ui.adapter.AlreadyExistsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil

View file

@ -7,8 +7,10 @@ import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.* import android.widget.*
import androidx.compose.foundation.layout.PaddingValues
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -21,6 +23,7 @@ import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.FormatSorter import com.deniscerri.ytdl.util.FormatSorter
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
@ -31,46 +34,57 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.internal.format
import java.util.regex.Pattern import java.util.regex.Pattern
class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>? = null, private var _formats: List<List<Format>>? = null, private val _listener: OnFormatClickListener? = null) : BottomSheetDialogFragment() { class FormatSelectionBottomSheetDialog(
private val _items: List<DownloadItem?>? = null,
private val _listener: OnFormatClickListener? = null,
private val _multipleFormatsListener: OnMultipleFormatClickListener? = null
) : BottomSheetDialogFragment() {
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var infoUtil: InfoUtil private lateinit var infoUtil: InfoUtil
private lateinit var view: View
private lateinit var continueInBackgroundSnackBar : Snackbar
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var formatCollection: MutableList<List<Format>>
private lateinit var chosenFormats: List<Format>
private var selectedVideo : Format? = null
private lateinit var selectedAudios : MutableList<Format>
private lateinit var videoFormatList : LinearLayout private lateinit var videoFormatList : LinearLayout
private lateinit var audioFormatList : LinearLayout private lateinit var audioFormatList : LinearLayout
private lateinit var okBtn : Button private lateinit var okBtn : Button
private lateinit var refreshBtn: Button
private lateinit var videoTitle : TextView private lateinit var videoTitle : TextView
private lateinit var audioTitle : TextView private lateinit var audioTitle : TextView
private lateinit var chosenFormats: List<Format>
private var selectedVideo : Format? = null
private lateinit var selectedAudios : MutableList<Format>
private lateinit var sortBy : FormatSorting private lateinit var sortBy : FormatSorting
private lateinit var filterBy : FormatCategory private lateinit var filterBy : FormatCategory
private lateinit var filterBtn : Button private lateinit var filterBtn : Button
private lateinit var continueInBackgroundSnackBar : Snackbar
private lateinit var view: View
private var updateFormatsJob: Job? = null private var updateFormatsJob: Job? = null
private var isMissingFormats: Boolean = false
private var hasGenericFormats: Boolean = false private lateinit var items: MutableList<DownloadItem?>
private lateinit var formats: MutableList<Format>
private lateinit var items: List<DownloadItem?>
private lateinit var formats: List<List<Format>>
private lateinit var listener: OnFormatClickListener private lateinit var listener: OnFormatClickListener
private lateinit var multipleFormatsListener: OnMultipleFormatClickListener
private var currentFormatSource : String? = null private var currentFormatSource : String? = null
private lateinit var genericAudioFormats : List<Format>
private lateinit var genericVideoFormats : List<Format>
enum class FormatSorting { enum class FormatSorting {
filesize, container, codec, id filesize, container, codec, id
} }
@ -82,7 +96,6 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity().applicationContext) infoUtil = InfoUtil(requireActivity().applicationContext)
formatCollection = mutableListOf()
chosenFormats = listOf() chosenFormats = listOf()
selectedAudios = mutableListOf() selectedAudios = mutableListOf()
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
@ -101,9 +114,27 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
return return
} }
items = _items items = _items.distinctBy { it!!.url }.toMutableList()
formats = _formats!! if (items.size == 1) {
listener = _listener!! formats = items.first()!!.allFormats
}else{
val flatFormatCollection = items.map { it!!.allFormats }.flatten()
formats = flatFormatCollection.groupingBy { it.format_id }.eachCount()
.filter { it.value == items.size }
.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }
.map { it.value }.toMutableList()
}
_listener?.apply {
listener = this
}
_multipleFormatsListener?.apply {
multipleFormatsListener = this
}
genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources)
genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources)
sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!) sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!)
filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!) filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!)
@ -126,32 +157,25 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
okBtn = view.findViewById(R.id.format_ok) okBtn = view.findViewById(R.id.format_ok)
shimmers.visibility = View.GONE shimmers.visibility = View.GONE
hasGenericFormats = formats.first().isEmpty() || formats.last().any { it.format_id == "best" || it.format_id == "ba" } isMissingFormats = formats.isEmpty() && items.any { it!!.allFormats.isEmpty() }
filterBtn.isVisible = !hasGenericFormats
if (items.size > 1){ if (items.size > 1){
if (!isMissingFormats){
if (!hasGenericFormats){ chosenFormats = formats.mapTo(mutableListOf()) {it.copy()}
formatCollection.addAll(formats)
val flattenFormats = formats.flatten()
val commonFormats = flattenFormats.groupingBy { it.format_id }.eachCount().filter { it.value == items.size }.mapValues { flattenFormats.first { f -> f.format_id == it.key } }.map { it.value }
chosenFormats = commonFormats.mapTo(mutableListOf()) {it.copy()}
chosenFormats = when(items.first()?.type){ chosenFormats = when(items.first()?.type){
Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) } Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
else -> chosenFormats else -> chosenFormats
} }
chosenFormats.forEach { chosenFormats.forEach {
it.filesize = it.filesize = items.map { itm -> itm!!.allFormats }.flatten().filter { f -> f.format_id == it.format_id }.sumOf { itt -> itt.filesize }
flattenFormats.filter { f -> f.format_id == it.format_id }
.sumOf { itt -> itt.filesize }
} }
}else{ }else{
chosenFormats = formats.flatten() chosenFormats = formats
} }
addFormatsToView() addFormatsToView()
}else{ }else{
chosenFormats = formats.flatten() chosenFormats = formats
if(!hasGenericFormats){ if(!isMissingFormats){
if(items.first()?.type == Type.audio){ if(items.first()?.type == Type.audio){
chosenFormats = chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) } chosenFormats = chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
} }
@ -159,41 +183,22 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
addFormatsToView() addFormatsToView()
} }
val refreshBtn = view.findViewById<Button>(R.id.format_refresh) refreshBtn = view.findViewById(R.id.format_refresh)
if (!hasGenericFormats || items.isEmpty() || items.first()?.url?.isEmpty() == true) refreshBtn.visibility = View.GONE filterBtn.isVisible = chosenFormats.isNotEmpty() || items.all { it!!.url.isYoutubeURL() }
if (!isMissingFormats || items.isEmpty() || items.first()?.url?.isEmpty() == true) {
val formatSourceLinear = view.findViewById<MaterialCardView>(R.id.formatSourceLinear) refreshBtn.visibility = View.GONE
val formatSourceGroup = view.findViewById<RadioGroup>(R.id.format_source_group)
val youtubeURLMatcher = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val canSwitch = items.all { youtubeURLMatcher.matcher(it!!.url).find() }
if (canSwitch) {
val availableSources = resources.getStringArray(R.array.formats_source)
val availableSourcesValues = resources.getStringArray(R.array.formats_source_values)
val currentSource = sharedPreferences.getString("formats_source", "yt-dlp")
formatSourceLinear.isVisible = true
availableSources.forEachIndexed { idx, it ->
val radio = RadioButton(requireContext())
val tag = availableSourcesValues[idx]
radio.text = it
radio.tag = tag
radio.setOnClickListener { compoundButton ->
formatSourceGroup.clearCheck()
formatSourceGroup.check(compoundButton.id)
currentFormatSource = compoundButton.tag.toString()
refreshBtn.performClick()
}
formatSourceGroup.addView(radio)
}
formatSourceGroup.check(formatSourceGroup.children.firstOrNull { it.tag == currentSource }!!.id)
} }
refreshBtn.setOnClickListener { refreshBtn.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
if (items.size > 10){ val itemsThatHaveFormats = items.filter { it!!.allFormats.isNotEmpty() }
val itemsWithMissingFormats = items.filter { it!!.allFormats.isEmpty() }.ifEmpty { items }
if (itemsWithMissingFormats.size > 10){
continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_LONG) continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_LONG)
continueInBackgroundSnackBar.setAction(R.string.ok) { continueInBackgroundSnackBar.setAction(R.string.ok) {
listener.onContinueOnBackground() _multipleFormatsListener!!.onContinueOnBackground()
this@FormatSelectionBottomSheetDialog.dismiss() this@FormatSelectionBottomSheetDialog.dismiss()
} }
continueInBackgroundSnackBar.show() continueInBackgroundSnackBar.show()
@ -202,19 +207,19 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
chosenFormats = emptyList() chosenFormats = emptyList()
refreshBtn.isEnabled = false refreshBtn.isEnabled = false
formatSourceLinear.isVisible = false refreshBtn.isVisible = true
okBtn.isVisible = false
okBtn.isEnabled = false
filterBtn.isEnabled = false
formatListLinearLayout.visibility = View.GONE formatListLinearLayout.visibility = View.GONE
shimmers.visibility = View.VISIBLE shimmers.visibility = View.VISIBLE
shimmers.startShimmer() shimmers.startShimmer()
updateFormatsJob = launch { updateFormatsJob = launch(Dispatchers.IO) {
try{ try{
//simple download //simple download
if (items.size == 1) { if (items.size == 1) {
formatCollection.clear()
kotlin.runCatching { kotlin.runCatching {
val res = withContext(Dispatchers.IO){ val res = infoUtil.getFormats(items.first()!!.url, currentFormatSource)
infoUtil.getFormats(items.first()!!.url, currentFormatSource)
}
res.filter { it.format_note != "storyboard" } res.filter { it.format_note != "storyboard" }
chosenFormats = if (items.first()?.type == Type.audio) { chosenFormats = if (items.first()?.type == Type.audio) {
res.filter { it.format_note.contains("audio", ignoreCase = true) } res.filter { it.format_note.contains("audio", ignoreCase = true) }
@ -223,11 +228,11 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
} }
if (chosenFormats.isEmpty()) throw Exception() if (chosenFormats.isEmpty()) throw Exception()
formats = listOf(res) formats.clear()
formats.addAll(res)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
listener.onFormatsUpdated(formats) listener.onFormatsUpdated(res)
} }
formatCollection.add(res)
}.onFailure { err -> }.onFailure { err ->
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
UiUtil.handleResultResponse(requireActivity(), ResultViewModel.ResultsUiState( UiUtil.handleResultResponse(requireActivity(), ResultViewModel.ResultsUiState(
@ -240,26 +245,44 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
//list format filtering //list format filtering
}else{ }else{
var progress = "0/${items.size}" formats.clear()
formatCollection.clear() var progressInt = 0
val formatCollection = itemsThatHaveFormats.map { it!!.allFormats }.toMutableList()
var progress = "0/${itemsWithMissingFormats.size}"
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
refreshBtn.text = progress refreshBtn.text = progress
} }
withContext(Dispatchers.IO){
infoUtil.getFormatsMultiple(items.map { it!!.url }, currentFormatSource) { val res = infoUtil.getFormatsMultiple(itemsWithMissingFormats.map { it!!.url }, currentFormatSource) {
if (isActive) { if (!isActive) return@getFormatsMultiple
lifecycleScope.launch(Dispatchers.Main) {
formatCollection.add(it) if (it.unavailable) {
progress = "${formatCollection.size}/${items.size}" lifecycleScope.launch {
refreshBtn.text = progress multipleFormatsListener.onItemUnavailable(it.url)
items.removeAt(items.indexOfFirst { item -> item!!.url == it.url })
withContext(Dispatchers.Main) {
Snackbar.make(view, it.unavailableMessage, Snackbar.LENGTH_SHORT).show()
} }
} }
}else{
multipleFormatsListener.onFormatUpdated(it.url, it.formats)
items.firstOrNull { item -> item!!.url == it.url }?.apply {
allFormats.clear()
allFormats.addAll(it.formats)
}
progressInt++
lifecycleScope.launch(Dispatchers.Main) {
progress = "${progressInt}/${itemsWithMissingFormats.size}"
refreshBtn.text = progress
}
} }
} }
formats = formatCollection
withContext(Dispatchers.Main){ formatCollection.addAll(res)
listener.onFormatsUpdated(formats)
} if (!isActive) return@launch
val flatFormatCollection = formatCollection.flatten() val flatFormatCollection = formatCollection.flatten()
val commonFormats = val commonFormats =
@ -267,14 +290,13 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
.filter { it.value == items.size } .filter { it.value == items.size }
.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } } .mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }
.map { it.value } .map { it.value }
formats.addAll(commonFormats)
chosenFormats = commonFormats.filter { it.filesize != 0L } chosenFormats = commonFormats.filter { it.filesize != 0L }
.mapTo(mutableListOf()) { it.copy() } .mapTo(mutableListOf()) { it.copy() }
chosenFormats = when (items.first()?.type) { chosenFormats = when (items.first()?.type) {
Type.audio -> chosenFormats.filter { Type.audio -> chosenFormats.filter {
it.format_note.contains( it.vcodec.isBlank() || it.vcodec == "none"
"audio",
ignoreCase = true
)
} }
else -> chosenFormats else -> chosenFormats
@ -286,24 +308,25 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
.sumOf { itt -> itt.filesize } .sumOf { itt -> itt.filesize }
} }
} }
hasGenericFormats = formatCollection.size == 0 isMissingFormats = formats.isEmpty()
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
shimmers.visibility = View.GONE shimmers.visibility = View.GONE
shimmers.stopShimmer() shimmers.stopShimmer()
formatSourceLinear.isVisible = canSwitch
filterBtn.isVisible = !hasGenericFormats
addFormatsToView() addFormatsToView()
refreshBtn.isVisible = hasGenericFormats refreshBtn.isVisible = isMissingFormats
refreshBtn.isEnabled = hasGenericFormats refreshBtn.isEnabled = isMissingFormats
filterBtn.isEnabled = true
okBtn.isEnabled = true
formatListLinearLayout.visibility = View.VISIBLE formatListLinearLayout.visibility = View.VISIBLE
} }
}catch (e: Exception){ }catch (e: Exception){
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
refreshBtn.isEnabled = true refreshBtn.isEnabled = true
refreshBtn.text = getString(R.string.update_formats) filterBtn.isEnabled = true
okBtn.isEnabled = true
refreshBtn.text = getString(R.string.update)
formatListLinearLayout.visibility = View.VISIBLE formatListLinearLayout.visibility = View.VISIBLE
shimmers.visibility = View.GONE shimmers.visibility = View.GONE
formatSourceLinear.isVisible = canSwitch
shimmers.stopShimmer() shimmers.stopShimmer()
e.printStackTrace() e.printStackTrace()
@ -334,54 +357,92 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
filterSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) filterSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
filterSheet.setContentView(R.layout.format_category_sheet) filterSheet.setContentView(R.layout.format_category_sheet)
val all = filterSheet.findViewById<TextView>(R.id.all) //format filter
val suggested = filterSheet.findViewById<TextView>(R.id.suggested) filterSheet.findViewById<LinearLayout>(R.id.format_filter_linear)?.isVisible = !isMissingFormats
val smallest = filterSheet.findViewById<TextView>(R.id.smallest) if (!isMissingFormats) {
val generic = filterSheet.findViewById<TextView>(R.id.generic) val all = filterSheet.findViewById<TextView>(R.id.all)
val suggested = filterSheet.findViewById<TextView>(R.id.suggested)
val smallest = filterSheet.findViewById<TextView>(R.id.smallest)
val generic = filterSheet.findViewById<TextView>(R.id.generic)
val filterOptions = listOf(all!!, suggested!!,smallest!!, generic!!) val filterOptions = listOf(all!!, suggested!!,smallest!!, generic!!)
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
when(filterBy) { when(filterBy) {
FormatCategory.ALL -> all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) FormatCategory.ALL -> all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
FormatCategory.SUGGESTED -> { FormatCategory.SUGGESTED -> {
suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
}
FormatCategory.SMALLEST -> {
smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
}
FormatCategory.GENERIC -> {
generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
}
}
all.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.ALL
all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss()
}
suggested.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.SUGGESTED
suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss()
} }
FormatCategory.SMALLEST -> { smallest.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.SMALLEST
smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss()
} }
FormatCategory.GENERIC -> { generic.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.GENERIC
generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss()
} }
} }
all.setOnClickListener { //format source
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } val formatSourceLinear = filterSheet.findViewById<LinearLayout>(R.id.format_source_linear)!!
filterBy = FormatCategory.ALL val canSwitch = items.all { it!!.url.isYoutubeURL() }
all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) formatSourceLinear.isVisible = canSwitch
addFormatsToView() if (canSwitch) {
filterSheet.dismiss() val formatSourceOptions = mutableListOf<TextView>()
}
suggested.setOnClickListener { val availableSources = resources.getStringArray(R.array.formats_source)
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } val availableSourcesValues = resources.getStringArray(R.array.formats_source_values)
filterBy = FormatCategory.SUGGESTED val currentSource = currentFormatSource ?: sharedPreferences.getString("formats_source", "yt-dlp")
suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) formatSourceLinear.isVisible = true
addFormatsToView() availableSources.forEachIndexed { idx, it ->
filterSheet.dismiss() val txt = requireActivity().layoutInflater.inflate(R.layout.selectable_textview_filter, null) as TextView
} val tag = availableSourcesValues[idx]
smallest.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } txt.text = it
filterBy = FormatCategory.SMALLEST txt.tag = tag
smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) txt.setOnClickListener {
addFormatsToView() currentFormatSource = it.tag.toString()
filterSheet.dismiss() refreshBtn.performClick()
} filterSheet.dismiss()
generic.setOnClickListener { }
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) } formatSourceOptions.add(txt)
filterBy = FormatCategory.GENERIC formatSourceLinear.addView(txt)
generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0) }
addFormatsToView() formatSourceOptions.firstOrNull { it.tag == currentSource }?.apply {
filterSheet.dismiss() setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
}
} }
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
filterSheet.behavior.peekHeight = displayMetrics.heightPixels filterSheet.behavior.peekHeight = displayMetrics.heightPixels
@ -390,21 +451,26 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
} }
private fun returnFormats(){ private fun returnFormats(){
//simple video format selection
if (items.size == 1){ if (items.size == 1){
listener.onFormatClick(listOf(FormatTuple(selectedVideo, selectedAudios.ifEmpty { listOf(downloadViewModel.getFormat(chosenFormats, Type.audio)) }))) //simple video format selection
listener.onFormatClick(FormatTuple(selectedVideo, selectedAudios.ifEmpty { listOf(downloadViewModel.getFormat(chosenFormats, Type.audio)) }))
}else{ }else{
//playlist format selection //playlist format selection
val selectedFormats = mutableListOf<Format?>() val formatsToReturn = mutableListOf<MultipleItemFormatTuple>()
formatCollection.forEach { items.forEach {
selectedFormats.add(it.first{ f -> f.format_id == selectedVideo?.format_id}) formatsToReturn.add(
MultipleItemFormatTuple(
it!!.url,
FormatTuple(
it.allFormats.firstOrNull { f -> f.format_id == selectedVideo?.format_id },
selectedAudios.map { sa ->
it.allFormats.first { a -> a.format_id == sa.format_id }
}.ifEmpty { null }
)
)
)
} }
if (selectedFormats.isEmpty()) { multipleFormatsListener.onFormatClick(formatsToReturn)
items.forEach { _ ->
selectedFormats.add(selectedVideo)
}
}
listener.onFormatClick(selectedFormats.map { FormatTuple(it, selectedAudios) })
} }
} }
@ -466,7 +532,9 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
} }
} }
val canMultiSelectAudio = items.first()?.type == Type.video && finalFormats.find { it.format_note.contains("audio", ignoreCase = true) } != null
val canMultiSelectAudio = items.first()?.type == Type.video && finalFormats.find { it.vcodec.isBlank() || it.vcodec == "none" } != null
videoFormatList.removeAllViews() videoFormatList.removeAllViews()
audioFormatList.removeAllViews() audioFormatList.removeAllViews()
@ -495,9 +563,9 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
if (finalFormats.isEmpty()){ if (finalFormats.isEmpty()){
finalFormats = if (items.first()?.type == Type.audio){ finalFormats = if (items.first()?.type == Type.audio){
infoUtil.getGenericAudioFormats(requireContext().resources) genericAudioFormats
}else{ }else{
infoUtil.getGenericVideoFormats(requireContext().resources) genericVideoFormats
} }
} }
@ -533,18 +601,22 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
} }
}else{ }else{
if (items.size == 1){ if (items.size == 1){
listener.onFormatClick(listOf(FormatTuple(format, null))) listener.onFormatClick(FormatTuple(format, null))
}else{ }else{
val selectedFormats = mutableListOf<Format>() val formatsToReturn = mutableListOf<MultipleItemFormatTuple>()
formatCollection.forEach { val f = if (genericAudioFormats.contains(format) || genericVideoFormats.contains(format)) format else null
selectedFormats.add(it.firstOrNull{ f -> f.format_id == format.format_id} ?: format) items.forEach {
formatsToReturn.add(
MultipleItemFormatTuple(
it!!.url,
FormatTuple(
f ?: it.allFormats.firstOrNull { af -> af.format_id == format.format_id },
null
)
)
)
} }
if (selectedFormats.isEmpty()) { multipleFormatsListener.onFormatClick(formatsToReturn)
items.forEach { _ ->
selectedFormats.add(format)
}
}
listener.onFormatClick(selectedFormats.map { FormatTuple(it, null) })
} }
dismiss() dismiss()
} }
@ -594,25 +666,36 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
} }
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
cleanUp() cleanUp()
super.onDismiss(dialog)
} }
private fun cleanUp(){ private fun cleanUp(){
kotlin.runCatching { kotlin.runCatching {
updateFormatsJob?.cancel() updateFormatsJob?.cancel(CancellationException())
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("formatSheet")!!).commit() parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("formatSheet")!!).commit()
} }
} }
} }
interface OnFormatClickListener{ interface OnFormatClickListener{
fun onFormatClick(selectedFormats: List<FormatTuple>) fun onFormatClick(formatTuple: FormatTuple)
fun onContinueOnBackground() {} fun onFormatsUpdated(allFormats: List<Format>)
fun onFormatsUpdated(allFormats: List<List<Format>>)
} }
interface OnMultipleFormatClickListener{
fun onFormatClick(formatTuple: List<MultipleItemFormatTuple>)
fun onContinueOnBackground() {}
fun onFormatUpdated(url: String, formats: List<Format>)
fun onItemUnavailable(url: String)
}
class MultipleItemFormatTuple internal constructor(
var url: String,
var formatTuple: FormatTuple
)
class FormatTuple internal constructor( class FormatTuple internal constructor(
var format: Format?, var format: Format?,
var audioFormats: List<Format>? var audioFormats: List<Format>?

View file

@ -24,6 +24,7 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
@ -39,11 +40,11 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.ResultItem import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadMinifiedAdapter import com.deniscerri.ytdl.ui.adapter.ActiveDownloadMinifiedAdapter
import com.deniscerri.ytdl.ui.adapter.GenericDownloadAdapter import com.deniscerri.ytdl.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdl.util.Extensions.setFullScreen import com.deniscerri.ytdl.util.Extensions.setFullScreen
@ -452,127 +453,28 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
downloadViewModel.getItemByID(itemID) downloadViewModel.getItemByID(itemID)
} }
val bottomSheet = BottomSheetDialog(requireContext()) UiUtil.showDownloadItemDetailsCard(
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) item,
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet) requireActivity(),
val title = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title) DownloadRepository.Status.valueOf(item.status),
title!!.text = item.title.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" } removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author) sheet.hide()
author!!.text = item.author.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" } removeQueuedItem(itemID)
},
// BUTTON ---------------------------------- downloadItem = {
val btn = bottomSheet.findViewById<MaterialButton>(R.id.downloads_download_button_type) runBlocking{
downloadViewModel.queueDownloads(listOf(it))
when (item.type) {
DownloadViewModel.Type.audio -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_music)
}
DownloadViewModel.Type.video -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video)
}
else -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal)
}
}
val time = bottomSheet.findViewById<Chip>(R.id.time)
val formatNote = bottomSheet.findViewById<Chip>(R.id.format_note)
val container = bottomSheet.findViewById<Chip>(R.id.container_chip)
val codec = bottomSheet.findViewById<Chip>(R.id.codec)
val fileSize = bottomSheet.findViewById<Chip>(R.id.file_size)
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) time!!.visibility = View.GONE
else {
val calendar = Calendar.getInstance()
calendar.timeInMillis = item.downloadStartTime
time!!.text = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(calendar.time)
time.setOnClickListener {
UiUtil.showDatePicker(parentFragmentManager) {
bottomSheet.dismiss()
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(item.id)
item.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(item.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
}
} }
} },
} longClickDownloadButton = {
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility = Pair("downloadItem", it),
View.GONE Pair("result", downloadViewModel.createResultItemFromDownload(it)),
else formatNote!!.text = item.format.format_note Pair("type", it.type)
)
if (item.format.container != "") container!!.text = item.format.container.uppercase() )
else container!!.visibility = View.GONE },
scheduleButtonClick = {}
val codecText =
if (item.format.encoding != "") {
item.format.encoding.uppercase()
}else if (item.format.vcodec != "none" && item.format.vcodec != ""){
item.format.vcodec.uppercase()
} else {
item.format.acodec.uppercase()
}
if (codecText == "" || codecText == "none"){
codec!!.visibility = View.GONE
}else{
codec!!.visibility = View.VISIBLE
codec.text = codecText
}
val fileSizeReadable = FileUtil.convertFileSize(item.format.filesize)
if (fileSizeReadable == "?") fileSize!!.visibility = View.GONE
else fileSize!!.text = fileSizeReadable
val link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link)
val url = item.url
link!!.text = url
link.tag = itemID
link.setOnClickListener{
bottomSheet.dismiss()
UiUtil.openLinkIntent(requireContext(), item.url)
}
link.setOnLongClickListener{
bottomSheet.dismiss()
UiUtil.copyLinkToClipBoard(requireContext(), item.url)
true
}
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = itemID
remove.setOnClickListener{
bottomSheet.hide()
removeQueuedItem(itemID)
}
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
openFile!!.visibility = View.GONE
val downloadNow = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) downloadNow!!.visibility = View.GONE
else{
downloadNow!!.text = getString(R.string.download_now)
downloadNow.setOnClickListener {
bottomSheet.dismiss()
downloadViewModel.deleteDownload(item.id)
item.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(item.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
}
}
}
bottomSheet.show()
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
) )
} }
} }
@ -608,7 +510,6 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
lifecycleScope.launch { lifecycleScope.launch {
val id = itemID.toInt() val id = itemID.toInt()
YoutubeDL.getInstance().destroyProcessById(id.toString()) YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString())
notificationUtil.cancelDownloadNotification(id) notificationUtil.cancelDownloadNotification(id)
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){

View file

@ -15,14 +15,19 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ListView
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.setMargins
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -49,6 +54,7 @@ import com.deniscerri.ytdl.work.DownloadWorker
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils import com.google.android.material.badge.BadgeUtils
import com.google.android.material.badge.ExperimentalBadgeUtils import com.google.android.material.badge.ExperimentalBadgeUtils
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
@ -70,27 +76,15 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener, QueuedDownloadAdapter.OnItemClickListener { class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener {
private var fragmentView: View? = null private var fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var activeDownloadsLinear: LinearLayout //private lateinit var activeDownloadsLinear: LinearLayout
private lateinit var activeDownloadsNoResults: View
private lateinit var activeRecyclerView : RecyclerView private lateinit var activeRecyclerView : RecyclerView
private lateinit var activeDownloads : ActiveDownloadAdapter private lateinit var activeDownloads : ActiveDownloadAdapter
private lateinit var queuedDownloadsLinear: LinearLayout
private lateinit var queuedRecyclerView : RecyclerView
private lateinit var queuedDownloads : QueuedDownloadAdapter
private lateinit var queuedFilesizeCount : TextView
private var queuedTotalSize = 0
private lateinit var queuedDragHandleToggle : TextView
private var queuedActionMode : ActionMode? = null
private lateinit var inQueueLabel : MaterialButton
private lateinit var activeQueuedLinear: LinearLayout
private lateinit var notificationUtil: NotificationUtil private lateinit var notificationUtil: NotificationUtil
private lateinit var pause: ExtendedFloatingActionButton private lateinit var pause: ExtendedFloatingActionButton
private lateinit var resume: ExtendedFloatingActionButton private lateinit var resume: ExtendedFloatingActionButton
@ -119,34 +113,12 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
workManager = WorkManager.getInstance(requireContext()) workManager = WorkManager.getInstance(requireContext())
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
activeDownloadsLinear = view.findViewById(R.id.active_linear)
activeDownloadsNoResults = view.findViewById(R.id.active_no_results)
activeDownloads = ActiveDownloadAdapter(this,requireActivity()) activeDownloads = ActiveDownloadAdapter(this,requireActivity())
activeRecyclerView = view.findViewById(R.id.download_recyclerview) activeRecyclerView = view.findViewById(R.id.download_recyclerview)
activeRecyclerView.forceFastScrollMode() activeRecyclerView.forceFastScrollMode()
activeRecyclerView.adapter = activeDownloads activeRecyclerView.adapter = activeDownloads
activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
queuedDownloadsLinear = view.findViewById(R.id.queue_linear)
val itemTouchHelper = ItemTouchHelper(queuedDragDropHelper)
queuedDownloads = QueuedDownloadAdapter(this, requireActivity(), itemTouchHelper)
queuedRecyclerView = view.findViewById(R.id.queued_recyclerview)
queuedRecyclerView.forceFastScrollMode()
queuedRecyclerView.adapter = queuedDownloads
queuedRecyclerView.enableFastScroll()
itemTouchHelper.attachToRecyclerView(queuedRecyclerView)
queuedFilesizeCount = view.findViewById(R.id.filesize)
queuedDragHandleToggle = view.findViewById(R.id.drag)
inQueueLabel = view.findViewById(R.id.in_queue_label)
if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("queued")){
val swipeItemTouchHelper = ItemTouchHelper(queuedSwipeCallback)
swipeItemTouchHelper.attachToRecyclerView(queuedRecyclerView)
}
queuedRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
activeQueuedLinear = view.findViewById(R.id.active_queued_linear)
pause = view.findViewById(R.id.pause) pause = view.findViewById(R.id.pause)
pause.isVisible = false pause.isVisible = false
resume = view.findViewById(R.id.resume) resume = view.findViewById(R.id.resume)
@ -164,13 +136,13 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
notificationUtil.cancelDownloadNotification(it.id.toInt()) notificationUtil.cancelDownloadNotification(it.id.toInt())
} }
preferences.edit().putBoolean("paused_downloads", true).apply() preferences.edit().putBoolean("paused_downloads", true).apply()
pause.isEnabled = false pause.isVisible = false
resume.isEnabled = false
resume.isVisible = true
activeDownloads.notifyDataSetChanged() activeDownloads.notifyDataSetChanged()
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
delay(1000) delay(1000)
pause.isVisible = false resume.isEnabled = true
pause.isEnabled = true
resume.isVisible = true
} }
} }
@ -179,7 +151,9 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
resume.setOnClickListener { resume.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
preferences.edit().putBoolean("paused_downloads", false).apply() preferences.edit().putBoolean("paused_downloads", false).apply()
resume.isEnabled = false resume.isVisible = false
pause.isEnabled = false
pause.isVisible = true
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
downloadViewModel.resetActiveToQueued() downloadViewModel.resetActiveToQueued()
downloadViewModel.startDownloadWorker(listOf()) downloadViewModel.startDownloadWorker(listOf())
@ -189,16 +163,14 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
} }
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
delay(1000) delay(1000)
resume.isVisible = false pause.isEnabled = true
resume.isEnabled = true
pause.isVisible = true
} }
} }
} }
lifecycleScope.launch { lifecycleScope.launch {
val activeQueuedDownloadsCount = withContext(Dispatchers.IO) { val activeQueuedDownloadsCount = withContext(Dispatchers.IO) {
downloadViewModel.getActiveDownloadsCount() downloadViewModel.getActiveQueuedDownloadsCount()
} }
val pausedDownloads = preferences.getBoolean("paused_downloads", false) val pausedDownloads = preferences.getBoolean("paused_downloads", false)
pause.isVisible = activeQueuedDownloadsCount > 0 && !pausedDownloads pause.isVisible = activeQueuedDownloadsCount > 0 && !pausedDownloads
@ -207,8 +179,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.activeQueuedDownloadsCount.collectLatest { downloadViewModel.activeQueuedDownloadsCount.collectLatest {
activeQueuedLinear.isVisible = it > 0
noResults.isVisible = it == 0
if (it == 0) { if (it == 0) {
pause.isVisible = false pause.isVisible = false
resume.isVisible = false resume.isVisible = false
@ -218,57 +188,11 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.activeDownloads.collectLatest { downloadViewModel.activeDownloads.collectLatest {
activeDownloadsNoResults.isVisible = it.isEmpty() noResults.isVisible = it.isEmpty()
activeDownloads.submitList(it) activeDownloads.submitList(it)
} }
} }
lifecycleScope.launch {
downloadViewModel.queuedDownloads.collectLatest {
queuedDownloads.submitData(it)
}
}
queuedDownloads.addLoadStateListener { loadState ->
lifecycleScope.launch {
if (loadState.append.endOfPaginationReached )
{
if (queuedDownloads.itemCount < 1){
queuedFilesizeCount.visibility = View.GONE
}else{
val size = withContext(Dispatchers.IO){
FileUtil.convertFileSize(downloadViewModel.getQueuedCollectedFileSize())
}
if (size == "?") queuedFilesizeCount.visibility = View.GONE
else {
queuedFilesizeCount.visibility = View.VISIBLE
queuedFilesizeCount.text = "${getString(R.string.file_size)}: ~ $size"
}
}
}
}
}
lifecycleScope.launch {
downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Queued)).observe(viewLifecycleOwner){
queuedDownloadsLinear.isVisible = it > 0
queuedTotalSize = it
queuedDragHandleToggle.isVisible = it > 1
if (it > 0) {
val badgeDrawable = BadgeDrawable.create(requireContext())
badgeDrawable.number = it
badgeDrawable.isVisible = true
badgeDrawable.verticalOffset = 25
requireView().post {
BadgeUtils.attachBadgeDrawable(badgeDrawable, inQueueLabel)
}
}
}
}
queuedDragHandleToggle.setOnClickListener {
queuedDownloads.toggleShowDragHandle()
}
} }
//dont remove //dont remove
@ -280,14 +204,14 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
requireActivity().runOnUiThread { requireActivity().runOnUiThread {
try { try {
progressBar?.setProgressCompat(event.progress, true) progressBar?.setProgressCompat(event.progress, true)
if (event.progress > 95) {
pause.isEnabled = false
}
outputText?.text = event.output outputText?.text = event.output
}catch (ignored: Exception) {} }catch (ignored: Exception) {}
} }
} }
fun hidePauseBtn(){
pause.isVisible = false
}
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -351,398 +275,4 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
} }
} }
//QUEUED ITEMS FUNCTIONS
private val queuedContextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.queued_menu_context, menu)
mode.title = "${queuedDownloads.getSelectedObjectsCount(queuedTotalSize)} ${getString(R.string.selected)}"
return true
}
override fun onPrepareActionMode(
mode: ActionMode?,
menu: Menu?
): Boolean {
return false
}
override fun onActionItemClicked(
mode: ActionMode?,
item: MenuItem?
): Boolean {
return when (item!!.itemId) {
R.id.select_between -> {
lifecycleScope.launch {
val selectedIDs = getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(
DownloadRepository.Status.Queued).toListString())
}.toMutableList()
idsInMiddle.addAll(selectedIDs)
if (idsInMiddle.isNotEmpty()){
queuedDownloads.checkMultipleItems(idsInMiddle)
queuedActionMode?.title = "${idsInMiddle.count()} ${getString(R.string.selected)}"
}
mode?.menu?.findItem(R.id.select_between)?.isVisible = false
}
true
}
R.id.delete_results -> {
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
queuedDownloads.clearCheckedItems()
for (id in selectedObjects){
YoutubeDL.getInstance().destroyProcessById(id.toInt().toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
notificationUtil.cancelDownloadNotification(id.toInt())
}
downloadViewModel.deleteAllWithID(selectedObjects)
queuedActionMode?.finish()
}
}
deleteDialog.show()
true
}
R.id.select_all -> {
queuedDownloads.checkAll()
mode?.title = "(${queuedDownloads.getSelectedObjectsCount(queuedTotalSize)}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {
queuedDownloads.invertSelected()
val selectedObjects = queuedDownloads.getSelectedObjectsCount(queuedTotalSize)
queuedActionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
if (selectedObjects == 0) queuedActionMode?.finish()
true
}
R.id.up -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
queuedDownloads.clearCheckedItems()
withContext(Dispatchers.IO){
downloadViewModel.putAtTopOfQueue(selectedObjects)
}
queuedActionMode?.finish()
}
true
}
R.id.down -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
queuedDownloads.clearCheckedItems()
withContext(Dispatchers.IO){
downloadViewModel.putAtBottomOfQueue(selectedObjects)
}
queuedActionMode?.finish()
}
true
}
R.id.copy_urls -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
val urls = withContext(Dispatchers.IO){
downloadViewModel.getURLsByIds(selectedObjects)
}
UiUtil.copyToClipboard(urls.joinToString("\n"), requireActivity())
queuedActionMode?.finish()
}
true
}
else -> false
}
}
override fun onDestroyActionMode(mode: ActionMode?) {
queuedActionMode = null
queuedDownloads.clearCheckedItems()
}
suspend fun getSelectedIDs() : List<Long>{
return if (queuedDownloads.inverted || queuedDownloads.checkedItems.isEmpty()){
withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(queuedDownloads.checkedItems.toList(), listOf(
DownloadRepository.Status.Queued))
}
}else{
queuedDownloads.checkedItems.toList()
}
}
}
var movedToNewPositionID = 0L
private val queuedDragDropHelper: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPosition = viewHolder.bindingAdapterPosition
val toPosition = target.bindingAdapterPosition
movedToNewPositionID = target.itemView.tag.toString().toLong()
queuedDownloads.notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
override fun onSelectedChanged(
viewHolder: RecyclerView.ViewHolder?,
actionState: Int
) {
super.onSelectedChanged(viewHolder, actionState)
if (ItemTouchHelper.ACTION_STATE_DRAG == actionState) {
/**
* Change alpha, scale and elevation on drag.
*/
(viewHolder?.itemView as? MaterialCardView)?.also {
AnimatorSet().apply {
this.duration = 100L
this.interpolator = AccelerateDecelerateInterpolator()
playTogether(
UiUtil.getAlphaAnimator(it, 0.7f),
UiUtil.getScaleXAnimator(it, 1.02f),
UiUtil.getScaleYAnimator(it, 1.02f),
UiUtil.getElevationAnimator(it, R.dimen.elevation_6dp)
)
}.start()
}
}
}
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
super.clearView(recyclerView, viewHolder)
/**
* Clear alpha, scale and elevation after drag/swipe
*/
(viewHolder.itemView as? MaterialCardView)?.also {
AnimatorSet().apply {
this.duration = 100L
this.interpolator = AccelerateDecelerateInterpolator()
playTogether(
UiUtil.getAlphaAnimator(it, 1f),
UiUtil.getScaleXAnimator(it, 1f),
UiUtil.getScaleYAnimator(it, 1f),
UiUtil.getElevationAnimator(it, R.dimen.elevation_2dp)
)
}.start()
}
downloadViewModel.putAtPosition(viewHolder.itemView.tag.toString().toLong(), movedToNewPositionID)
}
override fun isLongPressDragEnabled(): Boolean {
return false
}
}
private var queuedSwipeCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) {
ItemTouchHelper.LEFT -> {
lifecycleScope.launch {
val deletedItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
queuedDownloads.notifyItemChanged(viewHolder.bindingAdapterPosition)
removeQueuedItem(deletedItem.id)
}
}
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
RecyclerViewSwipeDecorator.Builder(
requireContext(),
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
.addSwipeLeftBackgroundColor(Color.RED)
.addSwipeLeftActionIcon(R.drawable.baseline_delete_24)
.addSwipeRightBackgroundColor(
MaterialColors.getColor(
requireContext(),
R.attr.colorOnSurfaceInverse, Color.TRANSPARENT
)
)
.create()
.decorate()
super.onChildDraw(
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
}
}
override fun onQueuedCancelClick(itemID: Long) {
lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toInt().toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}.let {
it.status = DownloadRepository.Status.Cancelled.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(it)
}
}
}
}
override fun onMoveQueuedItemToTop(itemID: Long) {
lifecycleScope.launch {
downloadViewModel.putAtTopOfQueue(listOf(itemID))
}
}
override fun onMoveQueuedItemToBottom(itemID: Long) {
lifecycleScope.launch {
downloadViewModel.putAtBottomOfQueue(listOf(itemID))
}
}
override fun onQueuedCardClick(itemID: Long) {
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
UiUtil.showDownloadItemDetailsCard(
item,
requireActivity(),
DownloadRepository.Status.valueOf(item.status),
removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
sheet.hide()
removeQueuedItem(it.id)
},
downloadItem = {
downloadViewModel.deleteDownload(it.id)
it.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(it))
}
},
longClickDownloadButton = {
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) {
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(downloadItem.id)
downloadItem.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(downloadItem))
}
}
}
)
}
}
override fun onQueuedCardSelect(isChecked: Boolean, position: Int) {
lifecycleScope.launch {
val selectedObjects = queuedDownloads.getSelectedObjectsCount(queuedTotalSize)
if (queuedActionMode == null) queuedActionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(queuedContextualActionBar)
val now = System.currentTimeMillis()
queuedActionMode?.apply {
if (selectedObjects == 0){
this.finish()
}else{
this.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.up).isVisible = position > 0
this.menu.findItem(R.id.down).isVisible = position < queuedTotalSize
this.menu.findItem(R.id.select_between).isVisible = false
if(selectedObjects == 2){
val selectedIDs = queuedContextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Queued).toListString())
}
this.menu.findItem(R.id.select_between).isVisible = idsInMiddle.isNotEmpty()
}
}
}
}
}
private fun removeQueuedItem(id: Long){
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(id)
}
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
item.status = DownloadRepository.Status.Cancelled.toString()
lifecycleScope.launch(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
Snackbar.make(queuedRecyclerView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO) {
downloadViewModel.deleteDownload(item.id)
downloadViewModel.queueDownloads(listOf(item))
}
}.show()
}
deleteDialog.show()
}
}
} }

View file

@ -79,6 +79,8 @@ class DownloadQueueMainFragment : Fragment(){
val isInNavBar = NavbarUtil.getNavBarItems(requireActivity()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible } val isInNavBar = NavbarUtil.getNavBarItems(requireActivity()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
if (isInNavBar) { if (isInNavBar) {
topAppBar.navigationIcon = null topAppBar.navigationIcon = null
}else{
mainActivity.hideBottomNavigation()
} }
topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() } topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() }
@ -90,7 +92,7 @@ class DownloadQueueMainFragment : Fragment(){
overScrollMode = View.OVER_SCROLL_NEVER overScrollMode = View.OVER_SCROLL_NEVER
} }
val fragments = mutableListOf(ActiveDownloadsFragment(), /*QueuedDownloadsFragment(),*/ ScheduledDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment(), SavedDownloadsFragment()) val fragments = mutableListOf(ActiveDownloadsFragment(), QueuedDownloadsFragment(), ScheduledDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment(), SavedDownloadsFragment())
fragmentAdapter = DownloadListFragmentAdapter( fragmentAdapter = DownloadListFragmentAdapter(
childFragmentManager, childFragmentManager,
@ -104,11 +106,11 @@ class DownloadQueueMainFragment : Fragment(){
TabLayoutMediator(tabLayout, viewPager2) { tab, position -> TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
when (position) { when (position) {
0 -> tab.text = getString(R.string.running) 0 -> tab.text = getString(R.string.running)
// 1 -> tab.text = getString(R.string.in_queue) 1 -> tab.text = getString(R.string.in_queue)
1 -> tab.text = getString(R.string.scheduled) 2 -> tab.text = getString(R.string.scheduled)
2 -> tab.text = getString(R.string.cancelled) 3 -> tab.text = getString(R.string.cancelled)
3 -> tab.text = getString(R.string.errored) 4 -> tab.text = getString(R.string.errored)
4 -> tab.text = getString(R.string.saved) 5 -> tab.text = getString(R.string.saved)
} }
}.attach() }.attach()
@ -130,13 +132,12 @@ class DownloadQueueMainFragment : Fragment(){
initMenu() initMenu()
} }
}) })
mainActivity.hideBottomNavigation()
initMenu() initMenu()
if (arguments?.getString("tab") != null){ if (arguments?.getString("tab") != null){
tabLayout.getTabAt(3)!!.select() tabLayout.getTabAt(4)!!.select()
viewPager2.postDelayed( { viewPager2.postDelayed( {
viewPager2.setCurrentItem(3, false) viewPager2.setCurrentItem(4, false)
val reconfigureID = arguments?.getLong("reconfigure") val reconfigureID = arguments?.getLong("reconfigure")
reconfigureID?.apply { reconfigureID?.apply {
notificationUtil.cancelErroredNotification(this.toInt()) notificationUtil.cancelErroredNotification(this.toInt())
@ -166,30 +167,30 @@ class DownloadQueueMainFragment : Fragment(){
} }
} }
} }
// lifecycleScope.launch {
// downloadViewModel.queuedDownloadsCount.collectLatest {
// tabLayout.getTabAt(1)?.apply {
// createBadge(it)
// }
// }
// }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.scheduledDownloadsCount.collectLatest { downloadViewModel.queuedDownloadsCount.collectLatest {
tabLayout.getTabAt(1)?.apply { tabLayout.getTabAt(1)?.apply {
createBadge(it) createBadge(it)
} }
} }
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.cancelledDownloadsCount.collectLatest { downloadViewModel.scheduledDownloadsCount.collectLatest {
tabLayout.getTabAt(2)?.apply { tabLayout.getTabAt(2)?.apply {
createBadge(it) createBadge(it)
} }
} }
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.erroredDownloadsCount.collectLatest { downloadViewModel.cancelledDownloadsCount.collectLatest {
tabLayout.getTabAt(3)?.apply { tabLayout.getTabAt(3)?.apply {
createBadge(it)
}
}
}
lifecycleScope.launch {
downloadViewModel.erroredDownloadsCount.collectLatest {
tabLayout.getTabAt(4)?.apply {
removeBadge() removeBadge()
if (it > 0) createBadge(it) if (it > 0) createBadge(it)
} }
@ -197,7 +198,7 @@ class DownloadQueueMainFragment : Fragment(){
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.savedDownloadsCount.collectLatest { downloadViewModel.savedDownloadsCount.collectLatest {
tabLayout.getTabAt(4)?.apply { tabLayout.getTabAt(5)?.apply {
removeBadge() removeBadge()
if (it > 0) createBadge(it) if (it > 0) createBadge(it)
} }

View file

@ -116,6 +116,14 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
sortChip = view.findViewById(R.id.sortChip) sortChip = view.findViewById(R.id.sortChip)
selectedObjects = arrayListOf() selectedObjects = arrayListOf()
val isInNavBar = NavbarUtil.getNavBarItems(requireActivity()).any { n -> n.itemId == R.id.historyFragment && n.isVisible }
if (isInNavBar) {
topAppBar?.navigationIcon = null
}else{
mainActivity?.hideBottomNavigation()
}
topAppBar?.setNavigationOnClickListener { mainActivity?.onBackPressedDispatcher?.onBackPressed() }
historyList = mutableListOf() historyList = mutableListOf()
allhistoryList = mutableListOf() allhistoryList = mutableListOf()

View file

@ -211,7 +211,6 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
adapter.clearCheckedItems() adapter.clearCheckedItems()
for (id in selectedObjects){ for (id in selectedObjects){
YoutubeDL.getInstance().destroyProcessById(id.toInt().toString()) YoutubeDL.getInstance().destroyProcessById(id.toInt().toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
notificationUtil.cancelDownloadNotification(id.toInt()) notificationUtil.cancelDownloadNotification(id.toInt())
} }
downloadViewModel.deleteAllWithID(selectedObjects) downloadViewModel.deleteAllWithID(selectedObjects)
@ -222,20 +221,6 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
deleteDialog.show() deleteDialog.show()
true true
} }
R.id.download -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
adapter.clearCheckedItems()
for (id in selectedObjects){
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
}
withContext(Dispatchers.IO) {
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(selectedObjects)
}
actionMode?.finish()
}
true
}
R.id.select_all -> { R.id.select_all -> {
adapter.checkAll() adapter.checkAll()
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}" mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
@ -469,7 +454,6 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
downloadItem = { downloadItem = {
downloadViewModel.deleteDownload(it.id) downloadViewModel.deleteDownload(it.id)
it.downloadStartTime = 0 it.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
runBlocking { runBlocking {
downloadViewModel.queueDownloads(listOf(it)) downloadViewModel.queueDownloads(listOf(it))
} }
@ -493,7 +477,6 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show() Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(downloadItem.id) downloadViewModel.deleteDownload(downloadItem.id)
downloadItem.downloadStartTime = it.timeInMillis downloadItem.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
runBlocking { runBlocking {
downloadViewModel.queueDownloads(listOf(downloadItem)) downloadViewModel.queueDownloads(listOf(downloadItem))
} }
@ -507,16 +490,11 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
lifecycleScope.launch { lifecycleScope.launch {
val selectedObjects = adapter.getSelectedObjectsCount(totalSize) val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
val now = System.currentTimeMillis()
actionMode?.apply { actionMode?.apply {
if (selectedObjects == 0){ if (selectedObjects == 0){
this.finish() this.finish()
}else{ }else{
this.title = "$selectedObjects ${getString(R.string.selected)}" this.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.download).isVisible = withContext(Dispatchers.IO){
downloadViewModel.checkAllQueuedItemsAreScheduledAfterNow(adapter.checkedItems.toList(), adapter.inverted, now)
}
this.menu.findItem(R.id.up).isVisible = position > 0 this.menu.findItem(R.id.up).isVisible = position > 0
this.menu.findItem(R.id.down).isVisible = position < totalSize this.menu.findItem(R.id.down).isVisible = position < totalSize
this.menu.findItem(R.id.select_between).isVisible = false this.menu.findItem(R.id.select_between).isVisible = false

View file

@ -37,6 +37,7 @@ import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -138,7 +139,6 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
downloadItem = { downloadItem = {
downloadViewModel.deleteDownload(it.id) downloadViewModel.deleteDownload(it.id)
it.downloadStartTime = 0 it.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
runBlocking { runBlocking {
downloadViewModel.queueDownloads(listOf(it)) downloadViewModel.queueDownloads(listOf(it))
} }
@ -156,7 +156,6 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show() Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(downloadItem.id) downloadViewModel.deleteDownload(downloadItem.id)
downloadItem.downloadStartTime = it.timeInMillis downloadItem.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
runBlocking { runBlocking {
downloadViewModel.queueDownloads(listOf(downloadItem)) downloadViewModel.queueDownloads(listOf(downloadItem))
adapter.notifyItemChanged(position) adapter.notifyItemChanged(position)
@ -269,9 +268,6 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
lifecycleScope.launch { lifecycleScope.launch {
val selectedObjects = getSelectedIDs() val selectedObjects = getSelectedIDs()
adapter.clearCheckedItems() adapter.clearCheckedItems()
for (id in selectedObjects){
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
}
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(selectedObjects) downloadViewModel.resetScheduleTimeForItemsAndStartDownload(selectedObjects)
} }

View file

@ -36,6 +36,7 @@ class MoreFragment : Fragment() {
private lateinit var logs: TextView private lateinit var logs: TextView
private lateinit var commandTemplates: TextView private lateinit var commandTemplates: TextView
private lateinit var downloadQueue: TextView private lateinit var downloadQueue: TextView
private lateinit var downloads: TextView
private lateinit var cookies: TextView private lateinit var cookies: TextView
private lateinit var observeSources: TextView private lateinit var observeSources: TextView
private lateinit var terminateApp: TextView private lateinit var terminateApp: TextView
@ -59,14 +60,26 @@ class MoreFragment : Fragment() {
terminal = view.findViewById(R.id.terminal) terminal = view.findViewById(R.id.terminal)
logs = view.findViewById(R.id.logs) logs = view.findViewById(R.id.logs)
commandTemplates = view.findViewById(R.id.command_templates) commandTemplates = view.findViewById(R.id.command_templates)
downloads = view.findViewById(R.id.downloads)
downloadQueue = view.findViewById(R.id.download_queue) downloadQueue = view.findViewById(R.id.download_queue)
cookies = view.findViewById(R.id.cookies) cookies = view.findViewById(R.id.cookies)
observeSources = view.findViewById(R.id.observe_sources) observeSources = view.findViewById(R.id.observe_sources)
terminateApp = view.findViewById(R.id.terminate) terminateApp = view.findViewById(R.id.terminate)
settings = view.findViewById(R.id.settings) settings = view.findViewById(R.id.settings)
val showingTerminal = NavbarUtil.getNavBarItems(requireContext()).any { n -> n.itemId == R.id.terminalActivity && n.isVisible } var showingTerminal = false
var showingDownloads = false
var showingDownloadQueue = false
NavbarUtil.getNavBarItems(requireContext()).apply {
showingTerminal = any { n -> n.itemId == R.id.terminalActivity && n.isVisible }
showingDownloads = any { n -> n.itemId == R.id.historyFragment && n.isVisible }
showingDownloadQueue = any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
}
terminal.isVisible = !showingTerminal terminal.isVisible = !showingTerminal
downloads.isVisible = !showingDownloads
downloadQueue.isVisible = !showingDownloadQueue
terminal.setOnClickListener { terminal.setOnClickListener {
val intent = Intent(context, TerminalActivity::class.java) val intent = Intent(context, TerminalActivity::class.java)
@ -81,8 +94,9 @@ class MoreFragment : Fragment() {
findNavController().navigate(R.id.commandTemplatesFragment) findNavController().navigate(R.id.commandTemplatesFragment)
} }
val showingDownloadQueue = NavbarUtil.getNavBarItems(requireContext()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible } downloads.setOnClickListener {
downloadQueue.isVisible = !showingDownloadQueue findNavController().navigate(R.id.historyFragment)
}
downloadQueue.setOnClickListener { downloadQueue.setOnClickListener {
findNavController().navigate(R.id.downloadQueueMainFragment) findNavController().navigate(R.id.downloadQueueMainFragment)

View file

@ -34,6 +34,7 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DownloadLogFragment : Fragment() { class DownloadLogFragment : Fragment() {
@ -96,7 +97,9 @@ class DownloadLogFragment : Fragment() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val logItem = logViewModel.getItemById(id!!) val logItem = logViewModel.getItemById(id!!)
topAppBar.title = logItem.title withContext(Dispatchers.Main){
topAppBar.title = logItem.title
}
} }
content.isFocusable = true content.isFocusable = true

View file

@ -69,13 +69,29 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
archivePathResultLauncher.launch(intent) archivePathResultLauncher.launch(intent)
true true
} }
val scheduler = AlarmScheduler(requireContext())
val useAlarmManagerInsteadOfWorkManager = findPreference<SwitchPreferenceCompat>("use_alarm_for_scheduling")
useAlarmManagerInsteadOfWorkManager?.setOnPreferenceChangeListener { preference, newValue ->
var allowChange = true
if (newValue as Boolean){
if (!scheduler.canSchedule() && Build.VERSION.SDK_INT >= 31){
Intent().also { intent ->
intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
requireContext().startActivity(intent)
}
allowChange = false
}
}
allowChange
}
val useScheduler = findPreference<SwitchPreferenceCompat>("use_scheduler") val useScheduler = findPreference<SwitchPreferenceCompat>("use_scheduler")
val scheduleStart = findPreference<Preference>("schedule_start") val scheduleStart = findPreference<Preference>("schedule_start")
scheduleStart?.summary = preferences.getString("schedule_start", "00:00") scheduleStart?.summary = preferences.getString("schedule_start", "00:00")
val scheduleEnd = findPreference<Preference>("schedule_end") val scheduleEnd = findPreference<Preference>("schedule_end")
scheduleEnd?.summary = preferences.getString("schedule_end", "05:00") scheduleEnd?.summary = preferences.getString("schedule_end", "05:00")
val scheduler = AlarmScheduler(requireContext())
useScheduler?.setOnPreferenceChangeListener { preference, newValue -> useScheduler?.setOnPreferenceChangeListener { preference, newValue ->
var allowChange = true var allowChange = true

View file

@ -40,6 +40,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.App import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory
@ -476,4 +477,8 @@ object Extensions {
"<small>" + subtitle + "</small>" + "<br />", HtmlCompat.FROM_HTML_MODE_LEGACY) "<small>" + subtitle + "</small>" + "<br />", HtmlCompat.FROM_HTML_MODE_LEGACY)
} }
fun String.isYoutubeURL() : Boolean {
return Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)").matcher(this).find()
}
} }

View file

@ -57,33 +57,53 @@ class FormatSorter(private var context: Context) {
fun sortAudioFormats(formats: List<Format>) : List<Format> { fun sortAudioFormats(formats: List<Format>) : List<Format> {
val orderPreferences = getAudioFormatImportance() val orderPreferences = getAudioFormatImportance()
val fieldSorter: Comparator<Format> = Comparator { a, b -> val fieldSorter: Comparator<Format> = object : Comparator<Format> {
for (order in orderPreferences) { override fun compare(a: Format, b: Format): Int {
when(order) { for (order in orderPreferences) {
"smallsize" -> { val comparison = when (order) {
(a.filesize).compareTo(b.filesize) "smallsize" -> {
} (a.filesize).compareTo(b.filesize)
"id" -> {
(audioFormatIDPreference.contains(b.format_id)).compareTo(audioFormatIDPreference.contains(a.format_id))
}
"language" -> {
if (audioLanguagePreference.isBlank()) {
0
} }
else {
(b.lang?.contains(audioLanguagePreference) == true).compareTo(a.lang?.contains(audioLanguagePreference) == true) "id" -> {
(audioFormatIDPreference.contains(b.format_id)).compareTo(
audioFormatIDPreference.contains(a.format_id)
)
} }
"language" -> {
if (audioLanguagePreference.isBlank()) {
0
} else {
(b.lang?.contains(audioLanguagePreference) == true).compareTo(
a.lang?.contains(
audioLanguagePreference
) == true
)
}
}
"codec" -> {
("^(${audioCodecPreference}).+$".toRegex(RegexOption.IGNORE_CASE)
.matches(b.acodec))
.compareTo(
"^(${audioCodecPreference}).+$".toRegex(RegexOption.IGNORE_CASE)
.matches(a.acodec)
)
}
"container" -> {
(audioContainerPreference == b.container).compareTo(
audioContainerPreference == a.container
)
}
else -> 0
} }
"codec" -> { if (comparison != 0) return comparison
("^(${audioCodecPreference}).+$".toRegex(RegexOption.IGNORE_CASE).matches(b.acodec))
.compareTo("^(${audioCodecPreference}).+$".toRegex(RegexOption.IGNORE_CASE).matches(a.acodec))
}
"container" -> {
(audioContainerPreference == b.container).compareTo(audioContainerPreference == a.container)
}
} }
return 0
} }
0
} }
return formats.sortedWith(fieldSorter) return formats.sortedWith(fieldSorter)
} }
@ -152,4 +172,101 @@ class FormatSorter(private var context: Context) {
return formats.sortedWith(fieldSorter) return formats.sortedWith(fieldSorter)
} }
//OLD CODE JUST FOR STORAGE / REFERENCE
/*
* fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
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 ->
val 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 }
}
}
}
return requirements
}
//requirement and importance
@SuppressLint("RestrictedApi")
fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
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 ->
var 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
}
}
}
}
return requirements
}
* */
} }

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources import android.content.res.Resources
import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.Html import android.text.Html
import android.util.Log import android.util.Log
@ -20,13 +21,16 @@ import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.Extensions.getIntByAny import com.deniscerri.ytdl.util.Extensions.getIntByAny
import com.deniscerri.ytdl.util.Extensions.getStringByAny import com.deniscerri.ytdl.util.Extensions.getStringByAny
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.toStringDuration import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.work.TerminalDownloadWorker
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
@ -42,9 +46,11 @@ import java.util.regex.Pattern
class InfoUtil(private val context: Context) { class InfoUtil(private val context: Context) {
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var handler: Handler
init { init {
try { try {
handler = Handler(Looper.getMainLooper())
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
countryCODE = sharedPreferences.getString("locale", "")!! countryCODE = sharedPreferences.getString("locale", "")!!
if (countryCODE.isEmpty()) countryCODE = "US" if (countryCODE.isEmpty()) countryCODE = "US"
@ -131,6 +137,7 @@ class InfoUtil(private val context: Context) {
if (nextpage == "null") nextpage = "" if (nextpage == "null") nextpage = ""
return PlaylistTuple(nextpage, items) return PlaylistTuple(nextpage, items)
}catch (e: Exception){ }catch (e: Exception){
Log.e("PLST", e.toString())
return PlaylistTuple( return PlaylistTuple(
"", "",
getFromYTDL("https://www.youtube.com/playlist?list=$id") getFromYTDL("https://www.youtube.com/playlist?list=$id")
@ -186,7 +193,7 @@ class InfoUtil(private val context: Context) {
} }
return video return video
} }
private fun createVideoFromPipedJSON(obj: JSONObject, url: String): ResultItem? { private fun createVideoFromPipedJSON(obj: JSONObject, url: String, ignoreFormatPreference : Boolean = false): ResultItem? {
var video: ResultItem? = null var video: ResultItem? = null
try { try {
val id = getIDFromYoutubeURL(url) val id = getIDFromYoutubeURL(url)
@ -201,7 +208,7 @@ class InfoUtil(private val context: Context) {
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg" val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
val formats : ArrayList<Format> = ArrayList() val formats : ArrayList<Format> = ArrayList()
if(sharedPreferences.getString("formats_source", "yt-dlp") == "piped"){ if(sharedPreferences.getString("formats_source", "yt-dlp") == "piped" || ignoreFormatPreference){
if (obj.has("audioStreams")){ if (obj.has("audioStreams")){
val formatsInJSON = obj.getJSONArray("audioStreams") val formatsInJSON = obj.getJSONArray("audioStreams")
for (f in 0 until formatsInJSON.length()){ for (f in 0 until formatsInJSON.length()){
@ -440,11 +447,8 @@ class InfoUtil(private val context: Context) {
} }
fun getFormats(url: String, source : String? = null) : List<Format> { fun getFormats(url: String, source : String? = null) : List<Format> {
val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val m = p.matcher(url)
val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp") val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
if (m.find() && formatSource == "piped"){ if (url.isYoutubeURL() && formatSource == "piped"){
try { try {
val id = getIDFromYoutubeURL(url) val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id") val res = genericRequest("$pipedURL/streams/$id")
@ -455,11 +459,12 @@ class InfoUtil(private val context: Context) {
getFormatsFromYTDL(url) getFormatsFromYTDL(url)
} }
}else { }else {
val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id") val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id", true)
return item!!.formats return item!!.formats
} }
}catch(e: Exception) { }catch(e: Exception) {
println(e)
if (e is CancellationException) throw e if (e is CancellationException) throw e
return getFormatsFromYTDL(url) return getFormatsFromYTDL(url)
} }
@ -502,32 +507,53 @@ class InfoUtil(private val context: Context) {
val res = YoutubeDL.getInstance().execute(request) val res = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = try { val results: Array<String?> = try {
val lineSeparator = System.getProperty("line.separator") res.out.split(System.lineSeparator()).toTypedArray()
res.out.split(lineSeparator!!).toTypedArray()
} catch (e: Exception) { } catch (e: Exception) {
arrayOf(res.out) arrayOf(res.out)
} }
val json = results[0] val json = results[0]
val jsonArray = JSONArray(json) val jsonArray = kotlin.runCatching { JSONArray(json) }.getOrElse { JSONArray() }
return parseYTDLFormats(jsonArray) return parseYTDLFormats(jsonArray)
} }
fun getFormatsMultiple(urls: List<String>, source: String? = null, progress: (progress: List<Format>) -> Unit){ data class MultipleFormatProgress(
val urlsFile = File(context.cacheDir, "urls.txt") val url: String,
urlsFile.delete() val formats: List<Format>,
urlsFile.createNewFile() val unavailable : Boolean = false,
urls.forEach { val unavailableMessage: String = ""
urlsFile.appendText(it+"\n") )
}
suspend fun getFormatsMultiple(urls: List<String>, source: String? = null, progress: (progress: MultipleFormatProgress) -> Unit) : MutableList<MutableList<Format>> {
val formatCollection = mutableListOf<MutableList<Format>>()
val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp") val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)") val allYoutubeLinks = urls.all { it.isYoutubeURL() }
val allYoutubeLinks = urls.any {p.matcher(it).find() } if (allYoutubeLinks && formatSource == "piped") {
if (formatSource == "yt-dlp" || !allYoutubeLinks){ urls.forEach { url ->
val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id")
createVideoFromPipedJSON(res, url).apply {
formatCollection.add(this!!.formats)
progress(
MultipleFormatProgress(url, this.formats)
)
}
}
}
else {
val urlsFile = File(context.cacheDir, "urls.txt")
urlsFile.delete()
withContext(Dispatchers.IO) {
urlsFile.createNewFile()
}
urls.forEach {
urlsFile.appendText(it+"\n")
}
try { try {
val request = YoutubeDLRequest(emptyList()) val request = YoutubeDLRequest(emptyList())
request.addOption("--print", "%(formats)s") request.addOption("--print", "formats")
request.addOption("-a", urlsFile.absolutePath) request.addOption("-a", urlsFile.absolutePath)
request.addOption("--skip-download") request.addOption("--skip-download")
request.addOption("-R", "1") request.addOption("-R", "1")
@ -537,7 +563,6 @@ class InfoUtil(private val context: Context) {
request.addOption("-4") request.addOption("-4")
} }
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) request.addOption("--cookies", it)
@ -557,35 +582,59 @@ class InfoUtil(private val context: Context) {
} }
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp") request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
val txt = parseYTDLRequestString(request)
println(txt)
var urlIdx = 0
YoutubeDL.getInstance().execute(request){ progress, _, line -> YoutubeDL.getInstance().execute(request){ progress, _, line ->
try{ try{
if (line.isNotBlank()){ if (line.isNotBlank()){
val listOfStrings = JSONArray(line) val url = urls[urlIdx]
progress(parseYTDLFormats(listOfStrings)) println(line)
println(url)
if (line.contains("unavailable")) {
progress(MultipleFormatProgress(url, listOf(), true, line))
}else{
val formatsJSON = JSONArray(line)
val formats = parseYTDLFormats(formatsJSON)
formatCollection.add(formats)
progress(MultipleFormatProgress(url, formats))
}
} }
}catch (e: Exception){ }catch (e: Exception){
progress(emptyList()) Log.e("GET MULTIPLE FORMATS", e.toString())
} }
urlIdx++
} }
} catch (e: Exception) { } catch (e: Exception) {
Looper.prepare().run { e.message?.split(System.lineSeparator())?.apply {
this.forEach { line ->
println(line)
if (line.contains("unavailable")) {
kotlin.runCatching {
val id = Regex("""\[.*?\] (\w+):""").find(line)!!.groupValues[1]
val url = urls.first { it.contains(id) }
progress(MultipleFormatProgress(url, listOf(), true, line))
delay(500)
}
}
}
}
handler.post {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
} }
e.printStackTrace() e.printStackTrace()
} }
}else{
urls.forEach { urlsFile.delete()
val id = getIDFromYoutubeURL(it)
val res = genericRequest("$pipedURL/streams/$id")
createVideoFromPipedJSON(res, it)?.apply {
progress(this.formats)
}
}
} }
urlsFile.delete() return formatCollection
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
@ -656,7 +705,7 @@ class InfoUtil(private val context: Context) {
for (result in results) { for (result in results) {
if (result.isNullOrBlank()) continue if (result.isNullOrBlank()) continue
val jsonObject = JSONObject(result) val jsonObject = JSONObject(result)
var title = jsonObject.getStringByAny("alt_title", "title", "webpage_url_basename") val title = jsonObject.getStringByAny("alt_title", "title", "webpage_url_basename")
if (title == "[Private video]" || title == "[Deleted video]") continue if (title == "[Private video]" || title == "[Deleted video]") continue
var author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id") var author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id")
@ -789,7 +838,6 @@ class InfoUtil(private val context: Context) {
} }
val formatProper = Gson().fromJson(format.toString(), Format::class.java) val formatProper = Gson().fromJson(format.toString(), Format::class.java)
Log.e("NOTE", format.toString())
if (formatProper.format_note == null) continue if (formatProper.format_note == null) continue
if (format.has("format_note")){ if (format.has("format_note")){
@ -956,16 +1004,13 @@ class InfoUtil(private val context: Context) {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest { fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest {
lateinit var request : YoutubeDLRequest val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){
val matchFilter = mutableListOf<String>()
request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){
YoutubeDLRequest(downloadItem.url) YoutubeDLRequest(downloadItem.url)
}else{ }else{
YoutubeDLRequest(downloadItem.playlistURL!!).apply { YoutubeDLRequest(downloadItem.playlistURL!!).apply {
if(downloadItem.playlistIndex == null){ if(downloadItem.playlistIndex == null){
val matchPortion = downloadItem.url.split("/").last().split("=").last().split("&").first() val matchPortion = downloadItem.url.split("/").last().split("=").last().split("&").first()
matchFilter.add("id~='${matchPortion}'") addOption("--match-filter", "id~='${matchPortion}'")
}else{ }else{
addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}") addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}")
} }
@ -1131,6 +1176,7 @@ class InfoUtil(private val context: Context) {
when(type){ when(type){
DownloadViewModel.Type.audio -> { DownloadViewModel.Type.audio -> {
val supportedContainers = context.resources.getStringArray(R.array.audio_containers) val supportedContainers = context.resources.getStringArray(R.array.audio_containers)
var abrSort = ""
var audioQualityId : String = downloadItem.format.format_id var audioQualityId : String = downloadItem.format.format_id
if (audioQualityId.isBlank() || listOf("0", context.getString(R.string.best_quality), "ba", "best", "").contains(audioQualityId)){ if (audioQualityId.isBlank() || listOf("0", context.getString(R.string.best_quality), "ba", "best", "").contains(audioQualityId)){
@ -1138,7 +1184,8 @@ class InfoUtil(private val context: Context) {
}else if (listOf(context.getString(R.string.worst_quality), "wa", "worst").contains(audioQualityId)){ }else if (listOf(context.getString(R.string.worst_quality), "wa", "worst").contains(audioQualityId)){
audioQualityId = "wa/w" audioQualityId = "wa/w"
}else if(audioQualityId.contains("kbps_ytdlnisgeneric")){ }else if(audioQualityId.contains("kbps_ytdlnisgeneric")){
matchFilter.add("abr<=${audioQualityId.split("kbps")[0]}")
abrSort = audioQualityId.split("kbps")[0]
audioQualityId = "" audioQualityId = ""
}else{ }else{
audioQualityId += "/ba/b" audioQualityId += "/ba/b"
@ -1170,6 +1217,10 @@ class InfoUtil(private val context: Context) {
formatSorting.append(",acodec:$aCodecPref") formatSorting.append(",acodec:$aCodecPref")
} }
if (abrSort.isNotBlank()){
formatSorting.append(",abr~${abrSort}")
}
if(ext.isNotBlank()){ if(ext.isNotBlank()){
if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){ if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){
request.addOption("--audio-format", ext) request.addOption("--audio-format", ext)
@ -1284,8 +1335,9 @@ class InfoUtil(private val context: Context) {
val preferredAudioLanguage = sharedPreferences.getString("audio_language", "")!! val preferredAudioLanguage = sharedPreferences.getString("audio_language", "")!!
if (downloadItem.videoPreferences.removeAudio) audioF = "" if (downloadItem.videoPreferences.removeAudio) audioF = ""
var abrSort = ""
if(audioF.contains("kbps_ytdlnisgeneric")){ if(audioF.contains("kbps_ytdlnisgeneric")){
matchFilter.add("abr<=${audioF.split("kbps")[0]}") abrSort = audioF.split("kbps")[0]
audioF = "" audioF = ""
} }
@ -1403,6 +1455,7 @@ class InfoUtil(private val context: Context) {
if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref") if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref")
if (cont.isNotBlank()) append(",vext:$cont") if (cont.isNotBlank()) append(",vext:$cont")
if (acont.isNotBlank()) append(",aext:$acont") if (acont.isNotBlank()) append(",aext:$acont")
if (abrSort.isNotBlank()) append(",abr~${abrSort}")
if (this.isNotBlank()){ if (this.isNotBlank()){
request.addOption("-S", "+hasaud$this") request.addOption("-S", "+hasaud$this")
} }
@ -1466,10 +1519,6 @@ class InfoUtil(private val context: Context) {
else -> {} else -> {}
} }
if (matchFilter.isNotEmpty()) {
request.addOption("--match-filter", matchFilter.joinToString(" & "))
}
if (downloadItem.extraCommands.isNotBlank() && downloadItem.type != DownloadViewModel.Type.command){ if (downloadItem.extraCommands.isNotBlank() && downloadItem.type != DownloadViewModel.Type.command){
val cache = File(FileUtil.getCachePath(context)) val cache = File(FileUtil.getCachePath(context))
cache.mkdirs() cache.mkdirs()
@ -1546,7 +1595,8 @@ class InfoUtil(private val context: Context) {
fun getGenericVideoFormats(resources: Resources) : MutableList<Format>{ fun getGenericVideoFormats(resources: Resources) : MutableList<Format>{
val formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() } val formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() }
val videoFormats = resources.getStringArray(R.array.video_formats_values) val videoFormatsValues = resources.getStringArray(R.array.video_formats_values)
val videoFormats = resources.getStringArray(R.array.video_formats)
val formats = mutableListOf<Format>() val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("video_format", "") val containerPreference = sharedPreferences.getString("video_format", "")
val audioCodecPreference = sharedPreferences.getString("audio_codec", "")!!.run { val audioCodecPreference = sharedPreferences.getString("audio_codec", "")!!.run {
@ -1565,7 +1615,7 @@ class InfoUtil(private val context: Context) {
videoCodecs[videoCodecsValues.indexOf(this)] videoCodecs[videoCodecsValues.indexOf(this)]
} }
} }
videoFormats.forEach { formats.add(Format(it, containerPreference!!,videoCodecPreference,audioCodecPreference, "",0, it.split("_")[0])) } videoFormatsValues.forEachIndexed { index, it -> formats.add(Format(it, containerPreference!!,videoCodecPreference,audioCodecPreference, "",0, videoFormats[index])) }
formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.preferred_format_id),"", "",1, it)) } formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.preferred_format_id),"", "",1, it)) }
return formats return formats
} }
@ -1574,7 +1624,7 @@ class InfoUtil(private val context: Context) {
class PlaylistTuple internal constructor( class PlaylistTuple internal constructor(
var nextPageToken: String, var nextPageToken: String,
var videos: ArrayList<ResultItem> var videos: ArrayList<ResultItem>,
) )
companion object { companion object {

View file

@ -9,12 +9,9 @@ import android.view.MenuItem
import android.widget.PopupMenu import android.widget.PopupMenu
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.isGone
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.navigation.NavigationView
import java.util.*
object NavbarUtil { object NavbarUtil {
@ -28,7 +25,7 @@ object NavbarUtil {
private var navItems = mapOf( private var navItems = mapOf(
"Home" to R.id.homeFragment, "Home" to R.id.homeFragment,
"History" to R.id.historyFragment, "History" to R.id.historyFragment,
"Downloads" to R.id.downloadQueueMainFragment, "Queue" to R.id.downloadQueueMainFragment,
"Terminal" to R.id.terminalActivity, "Terminal" to R.id.terminalActivity,
"More" to R.id.moreFragment "More" to R.id.moreFragment
) )
@ -36,10 +33,11 @@ object NavbarUtil {
fun getStartFragmentId(context: Context) : Int { fun getStartFragmentId(context: Context) : Int {
val pref = settings.getString("start_destination", "")!! val pref = settings.getString("start_destination", "")!!
val items = getDefaultNavBarItems(context) val items = getDefaultNavBarItems(context)
val navBarHasPref = getNavBarPrefs().contains(items.indexOfFirst { it.itemId == navItems[pref] }.toString())
return if (pref == "") { return if (pref == "") {
R.id.homeFragment R.id.homeFragment
}else { }else {
items.firstOrNull { it.itemId == navItems[pref] }!!.itemId items.firstOrNull { it.itemId == navItems[pref] && navBarHasPref }?.itemId ?: R.id.homeFragment
} }
} }
@ -83,7 +81,7 @@ object NavbarUtil {
private fun getNavBarPrefs(): List<String> { private fun getNavBarPrefs(): List<String> {
return settings return settings
.getString("navigation_bar", "Home,History,More")!! .getString("navigation_bar", "0,1,2,-3,-4")!!
.split(",") .split(",")
} }

View file

@ -22,6 +22,21 @@ class AlarmScheduler(private val context: Context) {
private val preferences = PreferenceManager.getDefaultSharedPreferences(context) private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
fun scheduleAt(at: Long) {
alarmManager?.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
at,
PendingIntent.getBroadcast(
context,
1,
Intent(context, ScheduleAlarmReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
}
@SuppressLint("ScheduleExactAlarm") @SuppressLint("ScheduleExactAlarm")
fun schedule() { fun schedule() {
cancel() cancel()

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="?android:colorAccent" android:pathData="M22,5.72l-4.6,-3.86 -1.29,1.53 4.6,3.86L22,5.72zM7.88,3.39L6.6,1.86 2,5.71l1.29,1.53 4.59,-3.85zM12.5,8L11,8v6l4.75,2.85 0.75,-1.23 -4,-2.37L12.5,8zM12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>

View file

@ -58,6 +58,7 @@
app:icon="@drawable/ic_music" app:icon="@drawable/ic_music"
app:iconGravity="textStart" app:iconGravity="textStart"
app:iconPadding="0dp" app:iconPadding="0dp"
android:contentDescription="@string/preferred_download_type"
app:iconSize="11dp" app:iconSize="11dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

View file

@ -1,9 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.core.widget.NestedScrollView android:layout_width="match_parent"
android:layout_width="match_parent" android:scrollbars="none"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:orientation="vertical"
android:layout_height="match_parent"> android:layout_height="wrap_content"
android:fillViewport="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -127,4 +132,4 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.core.widget.NestedScrollView>

View file

@ -1,19 +1,30 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout style="@style/Widget.Material3.BottomSheet" <LinearLayout style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout <LinearLayout
android:id="@+id/format_filter_linear"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/all" android:layout_width="wrap_content"
android:text="@string/format_filter"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="10dp"
android:layout_marginTop="20dp" android:layout_marginTop="20dp"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/all"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
@ -73,4 +84,24 @@
</LinearLayout> </LinearLayout>
</FrameLayout> <LinearLayout
android:id="@+id/format_source_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="10dp"
android:layout_marginTop="20dp"
android:text="@string/format_source"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>

View file

@ -75,7 +75,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:autoLink="all" android:autoLink="all"
android:text="@string/update_formats" android:text="@string/update"
android:contentDescription="@string/update_formats"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
app:icon="@drawable/ic_refresh" app:icon="@drawable/ic_refresh"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -166,44 +167,6 @@
android:orientation="vertical" /> android:orientation="vertical" />
</LinearLayout> </LinearLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/formatSourceLinear"
android:orientation="vertical"
android:layout_width="match_parent"
android:visibility="gone"
android:layout_margin="10dp"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/format_source_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="10dp"
android:singleLine="false"
android:text="@string/format_source"
android:textSize="12sp" />
<RadioGroup
android:id="@+id/format_source_group"
android:layout_marginHorizontal="20dp"
android:checkedButton="@+id/radio_button_1"
android:layout_width="match_parent"
android:layout_marginBottom="10dp"
android:layout_height="wrap_content">
</RadioGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View file

@ -4,109 +4,13 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.core.widget.NestedScrollView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/download_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:scrollbars="none"
<LinearLayout android:clipToPadding="false"
android:id="@+id/active_queued_linear" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
android:orientation="vertical"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/active_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/active_no_results"
layout="@layout/no_results" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/download_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pause_resume" />
</LinearLayout>
<LinearLayout
android:id="@+id/queue_linear"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content"
android:minHeight="200dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_marginTop="20dp"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/in_queue_label"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5dp"
android:textColor="?android:colorPrimary"
android:text="@string/in_queue"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/drag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
app:drawableStartCompat="@drawable/ic_drag_handle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/filesize"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:paddingBottom="10dp"
android:text="@string/file_size"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/queued_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
android:scrollbars="none"
android:paddingBottom="100dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pause_resume">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/pause" android:id="@+id/pause"

View file

@ -16,6 +16,7 @@
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/history_toolbar" android:id="@+id/history_toolbar"
app:navigationIcon="@drawable/ic_back"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:title="@string/downloads" app:title="@string/downloads"

View file

@ -69,6 +69,22 @@
android:paddingHorizontal="15dp" android:paddingHorizontal="15dp"
app:drawableStartCompat="@drawable/ic_baseline_keyboard_arrow_right_24" /> app:drawableStartCompat="@drawable/ic_baseline_keyboard_arrow_right_24" />
<TextView
android:id="@+id/downloads"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="35dp"
android:textSize="16sp"
android:paddingVertical="15dp"
android:text="@string/downloads"
android:textStyle="bold"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:paddingHorizontal="15dp"
app:drawableTint="?android:colorAccent"
app:drawableStartCompat="@drawable/ic_downloads" />
<TextView <TextView
android:id="@+id/download_queue" android:id="@+id/download_queue"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -14,8 +14,11 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:text="@string/sort_by" android:text="@string/sort_by"
android:textSize="12sp" android:textSize="14sp"
android:layout_margin="20dp" android:textStyle="bold"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="10dp"
android:layout_marginTop="20dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView

View file

@ -188,6 +188,7 @@
android:padding="0dp" android:padding="0dp"
app:cornerRadius="10dp" app:cornerRadius="10dp"
app:icon="@drawable/ic_video" app:icon="@drawable/ic_video"
android:contentDescription="@string/preferred_download_type"
app:iconTint="?attr/colorAccent" app:iconTint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

View file

@ -17,6 +17,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:contentDescription="@string/home"
android:src="@drawable/ic_home_outlined" android:src="@drawable/ic_home_outlined"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
@ -39,6 +40,7 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
android:singleLine="true" android:singleLine="true"
android:textDirection="locale"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp" android:textSize="18sp"
tools:text="Item" /> tools:text="Item" />

View file

@ -86,13 +86,15 @@
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:hint="@string/url" android:hint="@string/url"
app:endIconContentDescription="@string/import_from_clipboard"
app:endIconMode="custom" app:endIconMode="custom"
app:endIconDrawable="@drawable/ic_clipboard" app:endIconDrawable="@drawable/ic_clipboard"
style="@style/Widget.Material3.TextInputLayout.FilledBox"> style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
android:inputType="text" android:inputType="textMultiLine"
android:maxLines="3"
android:layout_height="wrap_content" android:layout_height="wrap_content"
/> />

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/source_template"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:drawablePadding="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:text="TEST"
app:drawableStartCompat="@drawable/empty"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:ignore="HardcodedText" />

View file

@ -43,13 +43,13 @@
<string-array name="video_formats"> <string-array name="video_formats">
<item>@string/best_quality</item> <item>@string/best_quality</item>
<item>2160p</item> <item>~2160p</item>
<item>1440p</item> <item>~1440p</item>
<item>1080p</item> <item>~1080p</item>
<item>720p</item> <item>~720p</item>
<item>480p</item> <item>~480p</item>
<item>360p</item> <item>~360p</item>
<item>240p</item> <item>~240p</item>
<item>@string/worst_quality</item> <item>@string/worst_quality</item>
</string-array> </string-array>
@ -66,13 +66,13 @@
</string-array> </string-array>
<string-array name="audio_formats"> <string-array name="audio_formats">
<item>best</item> <item>@string/best_quality</item>
<item>192kbps</item> <item>~192kbps</item>
<item>160kbps</item> <item>~160kbps</item>
<item>128kbps</item> <item>~128kbps</item>
<item>96kbps</item> <item>~96kbps</item>
<item>64kbps</item> <item>~64kbps</item>
<item>worst</item> <item>@string/worst_quality</item>
</string-array> </string-array>
<string-array name="audio_formats_values"> <string-array name="audio_formats_values">

View file

@ -408,4 +408,6 @@
<string name="queue">Queue</string> <string name="queue">Queue</string>
<string name="reset">Reset</string> <string name="reset">Reset</string>
<string name="restore_info">Press \'Restore\' to merge the saved data with your current data.\nPress \'Reset\' to erase and only use the saved data in the file.</string> <string name="restore_info">Press \'Restore\' to merge the saved data with your current data.\nPress \'Reset\' to erase and only use the saved data in the file.</string>
<string name="use_alarm_manager">Use AlarmManager instead of WorkManager for Scheduling</string>
<string name="use_alarm_manager_summary">Enable this if WorkManager is restricted by your device vendor or scheduled jobs are not too precise</string>
</resources> </resources>

View file

@ -161,6 +161,15 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/scheduling"> <PreferenceCategory app:title="@string/scheduling">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_access_alarm_24"
android:key="use_alarm_for_scheduling"
android:summary="@string/use_alarm_manager_summary"
app:title="@string/use_alarm_manager" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false" app:defaultValue="false"

View file

@ -8,7 +8,7 @@ buildscript {
appCompatVer = '1.6.1' appCompatVer = '1.6.1'
junitVer = '4.13.2' junitVer = '4.13.2'
androidJunitVer = '1.1.5' androidJunitVer = '1.1.5'
espressoVer = '3.5.1' espressoVer = '3.6.1'
jacksonVer = '2.9.8' jacksonVer = '2.9.8'
// supports java 1.6 // supports java 1.6
commonsIoVer = '2.5' commonsIoVer = '2.5'