multiple download card fixes
This commit is contained in:
parent
fa2bd2ed14
commit
80266888e3
48 changed files with 1421 additions and 1972 deletions
|
|
@ -130,10 +130,10 @@ dependencies {
|
|||
implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer"
|
||||
implementation "com.github.yausername.youtubedl-android:ffmpeg:$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:ffmpeg:0.16.0"
|
||||
// implementation "io.github.junkfood02.youtubedl-android:aria2c:0.16.0"
|
||||
|
||||
// implementation "io.github.junkfood02.youtubedl-android:library:0.16.1"
|
||||
// implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.16.1"
|
||||
// implementation "io.github.junkfood02.youtubedl-android:aria2c:0.16.1"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$appCompatVer"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||
|
|
@ -143,10 +143,10 @@ dependencies {
|
|||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$navVer"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$navVer"
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.test.ext:junit-ktx:1.1.5'
|
||||
implementation 'androidx.compose.ui:ui-android:1.6.5'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.test.ext:junit-ktx:1.2.1'
|
||||
implementation 'androidx.compose.ui:ui-android:1.6.8'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
testImplementation "junit:junit:$junitVer"
|
||||
androidTestImplementation "junit:junit:$junitVer"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidJunitVer"
|
||||
|
|
@ -167,10 +167,10 @@ dependencies {
|
|||
implementation "androidx.room:room-runtime:$roomVer"
|
||||
implementation "androidx.room:room-ktx:$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"
|
||||
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"
|
||||
androidTestImplementation 'com.google.truth:truth:1.1.5'
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ dependencies {
|
|||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
|
||||
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"
|
||||
|
||||
// For media playback using ExoPlayer
|
||||
|
|
|
|||
|
|
@ -258,13 +258,10 @@ class MainActivity : BaseActivity() {
|
|||
navigationView.visibilityChanged {
|
||||
if (it.isVisible){
|
||||
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()
|
||||
val intent = intent
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.deniscerri.ytdl.database.dao
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
|
|
@ -25,7 +26,7 @@ interface DownloadDao {
|
|||
fun getActiveDownloads() : Flow<List<DownloadItem>>
|
||||
|
||||
@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)")
|
||||
fun getDownloadsCountFlow(statuses: List<String>) : Flow<Int>
|
||||
|
|
@ -54,15 +55,18 @@ interface DownloadDao {
|
|||
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
|
||||
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'")
|
||||
suspend fun updateProcessingDownloadPath(path: String)
|
||||
|
||||
@Query("SELECT * FROM downloads WHERE status='Active'")
|
||||
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')")
|
||||
fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
|
||||
@Query("UPDATE downloads SET status='Queued' where status = 'Active'")
|
||||
|
|
@ -179,6 +183,13 @@ interface DownloadDao {
|
|||
@Upsert
|
||||
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")
|
||||
suspend fun setStatus(id: Long, status: String)
|
||||
|
||||
|
|
|
|||
|
|
@ -49,9 +49,15 @@ interface ResultDao {
|
|||
@Query("DELETE FROM results WHERE id=:id")
|
||||
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")
|
||||
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")
|
||||
fun getResultByID(id: Long): ResultItem?
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import com.deniscerri.ytdl.database.models.DownloadItem
|
|||
import com.deniscerri.ytdl.database.models.DownloadItemSimple
|
||||
import com.deniscerri.ytdl.util.Extensions.toListString
|
||||
import com.deniscerri.ytdl.util.FileUtil
|
||||
import com.deniscerri.ytdl.work.AlarmScheduler
|
||||
import com.deniscerri.ytdl.work.DownloadWorker
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
|
@ -42,7 +43,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
|
|||
pagingSourceFactory = {downloadDao.getAllDownloads()}
|
||||
)
|
||||
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(
|
||||
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
|
||||
pagingSourceFactory = {downloadDao.getQueuedDownloads()}
|
||||
|
|
@ -101,6 +102,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
|
|||
downloadDao.update(item)
|
||||
}
|
||||
|
||||
suspend fun updateAll(list: List<DownloadItem>) {
|
||||
downloadDao.updateAll(list)
|
||||
}
|
||||
|
||||
suspend fun updateWithoutUpsert(item: DownloadItem){
|
||||
kotlin.runCatching { downloadDao.updateWithoutUpsert(item) }
|
||||
}
|
||||
|
|
@ -122,6 +127,14 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
|
|||
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> {
|
||||
return downloadDao.getProcessingDownloadsList()
|
||||
}
|
||||
|
|
@ -130,10 +143,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
|
|||
return downloadDao.getActiveAndQueuedDownloadsList()
|
||||
}
|
||||
|
||||
suspend fun updateProcessingDownloadTime(time: Long) {
|
||||
downloadDao.updateProcessingDownloadTime(time)
|
||||
}
|
||||
|
||||
fun getActiveAndQueuedDownloadIDs() : List<Long> {
|
||||
return downloadDao.getActiveAndQueuedDownloadIDs()
|
||||
}
|
||||
|
|
@ -203,14 +212,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
|
|||
downloadDao.removeAllLogID()
|
||||
}
|
||||
|
||||
fun getFilteredProcessingDownloads(ids: List<Long>) : Flow<List<DownloadItem>> {
|
||||
return downloadDao.getProcessingDownloads()
|
||||
.map {
|
||||
it.filter { ids.contains(it.id) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) {
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
|
@ -230,12 +231,16 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
|
|||
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()
|
||||
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
else {
|
||||
//workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
}
|
||||
|
||||
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
|
||||
.addTag("download")
|
||||
|
|
@ -243,10 +248,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
|
|||
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
|
||||
.setInputData(inputData.build())
|
||||
|
||||
queuedItems.forEach {
|
||||
workRequest.addTag(it.id.toString())
|
||||
}
|
||||
|
||||
workManager.enqueueUniqueWork(
|
||||
System.currentTimeMillis().toString(),
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import android.util.Patterns
|
|||
import com.deniscerri.ytdl.database.dao.ResultDao
|
||||
import com.deniscerri.ytdl.database.models.DownloadItem
|
||||
import com.deniscerri.ytdl.database.models.ResultItem
|
||||
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
|
||||
import com.deniscerri.ytdl.util.InfoUtil
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
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 == nextPageToken) break
|
||||
nextPageToken = tmpToken
|
||||
delay(1000)
|
||||
} while (true)
|
||||
itemCount.value = items.size
|
||||
return items
|
||||
|
|
@ -117,6 +120,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
|||
resultDao.delete(item.id)
|
||||
}
|
||||
|
||||
suspend fun deleteByUrl(url: String) {
|
||||
resultDao.deleteByUrl(url)
|
||||
}
|
||||
|
||||
suspend fun deleteAll(){
|
||||
itemCount.value = 0
|
||||
resultDao.deleteAll()
|
||||
|
|
@ -134,6 +141,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
|||
return resultDao.getResultByURL(url)
|
||||
}
|
||||
|
||||
fun getAllByURL(url: String) : List<ResultItem> {
|
||||
return resultDao.getAllByURL(url)
|
||||
}
|
||||
|
||||
fun getAllByIDs(ids: List<Long>) : List<ResultItem> {
|
||||
return resultDao.getAllByIDs(ids)
|
||||
}
|
||||
|
|
@ -162,9 +173,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
|||
|
||||
private fun getQueryType(inputQuery: String) : SourceType {
|
||||
var type = SourceType.SEARCH_QUERY
|
||||
val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
|
||||
val m = p.matcher(inputQuery)
|
||||
if (m.find()) {
|
||||
if (inputQuery.isYoutubeURL()) {
|
||||
type = SourceType.YOUTUBE_VIDEO
|
||||
if (inputQuery.contains("playlist?list=")) {
|
||||
type = SourceType.YOUTUBE_PLAYLIST
|
||||
|
|
|
|||
|
|
@ -5,7 +5,13 @@ import android.app.Application
|
|||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Parcelable
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
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.ResultRepository
|
||||
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.FileUtil
|
||||
import com.deniscerri.ytdl.util.FormatSorter
|
||||
import com.deniscerri.ytdl.util.InfoUtil
|
||||
import com.deniscerri.ytdl.work.AlarmScheduler
|
||||
import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
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.util.Locale
|
||||
|
||||
|
|
@ -53,14 +67,15 @@ import java.util.Locale
|
|||
class DownloadViewModel(private val application: Application) : AndroidViewModel(application) {
|
||||
private val dbManager: DBManager
|
||||
val repository : DownloadRepository
|
||||
private val sharedDownloadViewModel: SharedDownloadViewModel
|
||||
private val sharedPreferences: SharedPreferences
|
||||
private val commandTemplateDao: CommandTemplateDao
|
||||
private val infoUtil : InfoUtil
|
||||
private val resources : Resources
|
||||
|
||||
val allDownloads : Flow<PagingData<DownloadItem>>
|
||||
val queuedDownloads : Flow<PagingData<DownloadItemSimple>>
|
||||
val activeDownloads : Flow<List<DownloadItem>>
|
||||
val processingDownloads : Flow<List<DownloadItem>>
|
||||
val processingDownloads : Flow<List<DownloadItemSimple>>
|
||||
val cancelledDownloads : Flow<PagingData<DownloadItemSimple>>
|
||||
val erroredDownloads : Flow<PagingData<DownloadItemSimple>>
|
||||
val savedDownloads : Flow<PagingData<DownloadItemSimple>>
|
||||
|
|
@ -74,7 +89,15 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
val savedDownloadsCount : 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 extraCommandsForVideo: String = ""
|
||||
|
|
@ -87,15 +110,19 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
auto, audio, video, command
|
||||
}
|
||||
|
||||
var processingItemsFlow : ProcessingItemsJob? = null
|
||||
private val urlsForAudioType = listOf(
|
||||
"music",
|
||||
"audio",
|
||||
"soundcloud"
|
||||
)
|
||||
|
||||
var processingItems = MutableStateFlow(false)
|
||||
var processingItemsJob : Job? = null
|
||||
|
||||
init {
|
||||
dbManager = DBManager.getInstance(application)
|
||||
dao = dbManager.downloadDao
|
||||
repository = DownloadRepository(dao)
|
||||
sharedDownloadViewModel = SharedDownloadViewModel(application)
|
||||
alreadyExistsUiState = sharedDownloadViewModel.alreadyExistsUiState
|
||||
historyRepository = HistoryRepository(dbManager.historyDao)
|
||||
resultRepository = ResultRepository(dbManager.resultDao, 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) {
|
||||
|
|
@ -155,15 +187,130 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
|
|
@ -187,7 +334,22 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
}
|
||||
|
||||
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>{
|
||||
|
|
@ -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 {
|
||||
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>{
|
||||
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 {
|
||||
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){
|
||||
updateProcessingJobData(ProcessingItemsJob(null, DownloadItem::class.java.toString(), itemIDs))
|
||||
val job = viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.deleteProcessing()
|
||||
processingItems.emit(true)
|
||||
try {
|
||||
itemIDs.forEachIndexed { idx, it ->
|
||||
val item = repository.getItemByID(it)
|
||||
if (processingItemsFlow?.job?.isCancelled == true) throw CancellationException()
|
||||
if (processingItemsJob?.isCancelled == true) throw CancellationException()
|
||||
|
||||
item.id = 0
|
||||
item.status = DownloadRepository.Status.Processing.toString()
|
||||
processingItemsFlow?.apply {
|
||||
val id = repository.insert(item)
|
||||
processingDownloadItemIDs.add(id)
|
||||
updateProcessingJobData(this)
|
||||
}
|
||||
repository.insert(item)
|
||||
}
|
||||
processingItems.emit(false)
|
||||
} catch (e: Exception) {
|
||||
deleteProcessing()
|
||||
updateProcessingJobData(null)
|
||||
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) {
|
||||
updateProcessingJobData(ProcessingItemsJob(null, ResultItem::class.java.toString(), itemIDs))
|
||||
val job = viewModelScope.launch(Dispatchers.IO) {
|
||||
repository.deleteProcessing()
|
||||
processingItems.emit(true)
|
||||
try {
|
||||
val toInsert = mutableListOf<DownloadItem>()
|
||||
itemIDs.forEach { id ->
|
||||
val item = resultRepository.getItemByID(id) ?: return@forEach
|
||||
val preferredType = getDownloadType(url = item.url).toString()
|
||||
|
|
@ -375,7 +576,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
))
|
||||
downloadItem.status = DownloadRepository.Status.Processing.toString()
|
||||
|
||||
if (processingItemsFlow?.job?.isCancelled == true) {
|
||||
if (processingItemsJob?.isCancelled == true) {
|
||||
throw CancellationException()
|
||||
}
|
||||
|
||||
|
|
@ -383,21 +584,20 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
downloadItem.status = DownloadRepository.Status.Queued.toString()
|
||||
queueDownloads(listOf(downloadItem))
|
||||
}else{
|
||||
processingItemsFlow?.apply {
|
||||
processingDownloadItemIDs.add(repository.insert(downloadItem))
|
||||
updateProcessingJobData(this)
|
||||
}
|
||||
toInsert.add(downloadItem)
|
||||
//repository.insert(downloadItem)
|
||||
}
|
||||
}
|
||||
repository.insertAll(toInsert)
|
||||
processingItems.emit(false)
|
||||
}catch (e: Exception) {
|
||||
deleteProcessing()
|
||||
updateProcessingJobData(null)
|
||||
processingItems.emit(false)
|
||||
}
|
||||
}
|
||||
|
||||
updateProcessingJobData(ProcessingItemsJob(job, ResultItem::class.java.toString(), itemIDs))
|
||||
processingItemsJob = job
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
val scheduledIds = repository.getScheduledDownloadIDs()
|
||||
scheduledIds.forEach {
|
||||
WorkManager.getInstance(application).cancelAllWorkByTag(it.toString())
|
||||
}
|
||||
repository.deleteScheduled()
|
||||
}
|
||||
|
||||
|
|
@ -451,7 +647,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
}
|
||||
|
||||
fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) {
|
||||
processingItemsFlow?.apply { this.job?.cancel(CancellationException()) }
|
||||
processingItemsJob?.apply { cancel(CancellationException()) }
|
||||
repository.cancelActiveQueued()
|
||||
}
|
||||
|
||||
|
|
@ -574,9 +770,193 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
queueDownloads(processingItems)
|
||||
}
|
||||
|
||||
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : List<SharedDownloadViewModel.AlreadyExistsIDs> {
|
||||
val ids = sharedDownloadViewModel.queueDownloads(items, ign)
|
||||
return ids
|
||||
suspend fun queueDownloads(items: List<DownloadItem>, ignoreDuplicates : Boolean = false) {
|
||||
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
|
||||
//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 {
|
||||
|
|
@ -599,17 +979,25 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
dao.updateProcessingtoSavedStatus()
|
||||
}
|
||||
|
||||
suspend fun updateProcessingFormat(selectedFormats: List<FormatTuple>): List<Long> {
|
||||
|
||||
fun updateAllProcessingFormats(formatTuples : List<MultipleItemFormatTuple>) = viewModelScope.launch(Dispatchers.IO) {
|
||||
val items = repository.getProcessingDownloads()
|
||||
items.forEachIndexed { index, i ->
|
||||
selectedFormats[index].format?.apply {
|
||||
i.format = this
|
||||
items.forEach {
|
||||
val ft = formatTuples.first { ft -> ft.url == it.url }.formatTuple
|
||||
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){
|
||||
|
|
@ -624,37 +1012,50 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
dao.updateProcessingDownloadPath(path)
|
||||
}
|
||||
|
||||
fun getProcessingDownloadsCount() : Int {
|
||||
return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Processing.toString()))
|
||||
}
|
||||
|
||||
fun getProcessingDownloads() : List<DownloadItem> {
|
||||
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>>) {
|
||||
val items = repository.getProcessingDownloads()
|
||||
items.forEachIndexed { index, i ->
|
||||
i.allFormats.clear()
|
||||
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) {
|
||||
runCatching {
|
||||
i.allFormats.addAll(formatCollection[index])
|
||||
}
|
||||
runCatching {
|
||||
resultRepository.getAllByURL(item.url).forEach {
|
||||
it.formats.clear()
|
||||
it.formats.addAll(list)
|
||||
resultRepository.update(it)
|
||||
}
|
||||
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(){
|
||||
val ids = dao.getProcessingDownloadsList().map { it.id }
|
||||
val ids = repository.getProcessingDownloads().map { it.id }
|
||||
dao.updateProcessingtoSavedStatus()
|
||||
|
||||
val id = System.currentTimeMillis().toInt()
|
||||
|
|
@ -685,12 +1086,21 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun updateProcessingDownloadTime(time: Long) {
|
||||
repository.updateProcessingDownloadTime(time)
|
||||
suspend fun updateProcessingDownloadTimeAndQueueScheduled(time: Long) {
|
||||
val processing = repository.getProcessingDownloads()
|
||||
processing.forEach {
|
||||
it.downloadStartTime = time
|
||||
it.status = DownloadRepository.Status.Scheduled.toString()
|
||||
}
|
||||
queueDownloads(processing)
|
||||
}
|
||||
|
||||
fun checkIfAllProcessingItemsHaveSameType() : Pair<Boolean, Type> {
|
||||
val types = dao.getProcessingDownloadTypes()
|
||||
if (types.isEmpty()) {
|
||||
return Pair(false, Type.command)
|
||||
}
|
||||
|
||||
return Pair(types.size == 1, Type.valueOf(types.first()))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -11,13 +11,14 @@ import android.widget.FrameLayout
|
|||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.util.Extensions.loadThumbnail
|
||||
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 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
|
||||
).build()) {
|
||||
private val onItemClickListener: OnItemClickListener
|
||||
|
|
@ -155,12 +157,12 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
|
|||
}
|
||||
|
||||
companion object {
|
||||
private val DIFF_CALLBACK: DiffUtil.ItemCallback<DownloadItem> = object : DiffUtil.ItemCallback<DownloadItem>() {
|
||||
override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
|
||||
private val DIFF_CALLBACK: DiffUtil.ItemCallback<DownloadItemSimple> = object : DiffUtil.ItemCallback<DownloadItemSimple>() {
|
||||
override fun areItemsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean {
|
||||
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 &&
|
||||
oldItem.author == newItem.author &&
|
||||
oldItem.type == newItem.type &&
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ class NavBarOptionsAdapter(
|
|||
val noHome = listOf(R.id.terminalActivity)
|
||||
holder.binding.apply {
|
||||
title.text = item.title
|
||||
title.contentDescription = item.title
|
||||
|
||||
|
||||
checkbox.isChecked = item.isVisible || essential.contains(item.itemId)
|
||||
checkbox.isEnabled = !essential.contains(item.itemId)
|
||||
home.setImageResource(
|
||||
|
|
@ -52,11 +55,8 @@ class NavBarOptionsAdapter(
|
|||
if (!checkbox.isChecked || selectedHomeTabId == item.itemId) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
val oldSelection = items.indexOfFirst { it.itemId == selectedHomeTabId }
|
||||
selectedHomeTabId = item.itemId
|
||||
listOf(position, oldSelection).forEach {
|
||||
notifyItemChanged(it)
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
checkbox.setOnClickListener {
|
||||
item.isVisible = checkbox.isChecked
|
||||
|
|
|
|||
|
|
@ -113,26 +113,28 @@ class ConfigureDownloadBottomSheetDialog(private val currentDownloadItem: Downlo
|
|||
viewPager2.adapter = fragmentAdapter
|
||||
viewPager2.isSaveFromParentEnabled = false
|
||||
|
||||
when(currentDownloadItem.type) {
|
||||
Type.audio -> {
|
||||
tabLayout.selectTab(tabLayout.getTabAt(0))
|
||||
viewPager2.setCurrentItem(0, false)
|
||||
}
|
||||
Type.video -> {
|
||||
if (isAudioOnly){
|
||||
tabLayout.getTabAt(0)!!.select()
|
||||
view.post {
|
||||
when(currentDownloadItem.type) {
|
||||
Type.audio -> {
|
||||
tabLayout.selectTab(tabLayout.getTabAt(0))
|
||||
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)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
tabLayout.selectTab(tabLayout.getTabAt(2))
|
||||
viewPager2.postDelayed( {
|
||||
viewPager2.setCurrentItem(2, false)
|
||||
}, 200)
|
||||
Type.video -> {
|
||||
if (isAudioOnly){
|
||||
tabLayout.getTabAt(0)!!.select()
|
||||
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)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
tabLayout.selectTab(tabLayout.getTabAt(2))
|
||||
viewPager2.postDelayed( {
|
||||
viewPager2.setCurrentItem(2, false)
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -226,26 +226,30 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
|
|||
val chosenFormat = downloadItem.format
|
||||
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null)
|
||||
val listener = object : OnFormatClickListener {
|
||||
override fun onFormatClick(item: List<FormatTuple>) {
|
||||
item.first().format?.apply {
|
||||
override fun onFormatClick(formatTuple: FormatTuple) {
|
||||
formatTuple.format?.apply {
|
||||
downloadItem.format = this
|
||||
UiUtil.populateFormatCard(requireContext(), formatCard, this, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
|
||||
override fun onFormatsUpdated(allFormats: List<Format>) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
resultItem?.apply {
|
||||
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)
|
||||
kotlin.runCatching {
|
||||
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
|
||||
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)
|
||||
val preferredFormat = downloadViewModel.getFormat(formats, Type.audio)
|
||||
downloadItem.format = preferredFormat
|
||||
|
|
@ -255,7 +259,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
|
|||
}
|
||||
formatCard.setOnClickListener{
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
|
|||
|
||||
//check if the item has formats and its audio-only
|
||||
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){
|
||||
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true
|
||||
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
|
||||
|
|
@ -216,26 +216,28 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
|
|||
viewPager2.adapter = fragmentAdapter
|
||||
viewPager2.isSaveFromParentEnabled = false
|
||||
|
||||
when(type) {
|
||||
Type.audio -> {
|
||||
tabLayout.getTabAt(0)!!.select()
|
||||
viewPager2.setCurrentItem(0, false)
|
||||
}
|
||||
Type.video -> {
|
||||
if (isAudioOnly){
|
||||
view.post {
|
||||
when(type) {
|
||||
Type.audio -> {
|
||||
tabLayout.getTabAt(0)!!.select()
|
||||
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)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
tabLayout.getTabAt(2)!!.select()
|
||||
viewPager2.postDelayed( {
|
||||
viewPager2.setCurrentItem(2, false)
|
||||
}, 200)
|
||||
Type.video -> {
|
||||
if (isAudioOnly){
|
||||
tabLayout.getTabAt(0)!!.select()
|
||||
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)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
tabLayout.getTabAt(2)!!.select()
|
||||
viewPager2.postDelayed( {
|
||||
viewPager2.setCurrentItem(2, false)
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -592,6 +594,15 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
|
|||
resultViewModel.updateFormatsResultData.collectLatest { formats ->
|
||||
if (formats == null) return@collectLatest
|
||||
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 {
|
||||
withContext(Dispatchers.Main){
|
||||
runCatching {
|
||||
|
|
|
|||
|
|
@ -201,9 +201,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
toggleLoading(true)
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
downloadViewModel.updateProcessingDownloadTime(cal.timeInMillis)
|
||||
downloadViewModel.deleteAllWithID(currentDownloadIDs)
|
||||
downloadViewModel.queueProcessingDownloads()
|
||||
downloadViewModel.updateProcessingDownloadTimeAndQueueScheduled(cal.timeInMillis)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
|
@ -231,10 +230,10 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
downloadViewModel.deleteAllWithID(currentDownloadIDs)
|
||||
downloadViewModel.moveProcessingToSavedCategory()
|
||||
}
|
||||
getProcessingItemsData()?.apply {
|
||||
this.job?.cancel(CancellationException())
|
||||
this.job = null
|
||||
}
|
||||
|
||||
downloadViewModel.processingItemsJob?.cancel(CancellationException())
|
||||
downloadViewModel.processingItemsJob = null
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
|
@ -242,29 +241,27 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
true
|
||||
}
|
||||
|
||||
val formatListener = object : OnFormatClickListener {
|
||||
override fun onFormatClick(selectedFormats: List<FormatTuple>) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
downloadViewModel.updateProcessingFormat(selectedFormats)
|
||||
}
|
||||
val formatListener = object : OnMultipleFormatClickListener {
|
||||
override fun onFormatClick(formatTuple: List<MultipleItemFormatTuple>) {
|
||||
downloadViewModel.updateAllProcessingFormats(formatTuple)
|
||||
}
|
||||
|
||||
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
downloadViewModel.updateProcessingAllFormats(allFormats)
|
||||
}
|
||||
override fun onFormatUpdated(url: String, formats: List<Format>) {
|
||||
downloadViewModel.updateProcessingFormatByUrl(url, formats)
|
||||
}
|
||||
|
||||
override fun onItemUnavailable(url: String) {
|
||||
downloadViewModel.removeUnavailableDownloadAndResultByURL(url)
|
||||
}
|
||||
|
||||
override fun onContinueOnBackground() {
|
||||
requireActivity().lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
downloadViewModel.continueUpdatingFormatsOnBackground()
|
||||
}
|
||||
getProcessingItemsData()?.apply {
|
||||
this.job?.cancel(CancellationException())
|
||||
this.job = null
|
||||
}
|
||||
downloadViewModel.processingItemsJob?.cancel(CancellationException())
|
||||
downloadViewModel.processingItemsJob = null
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
|
@ -421,20 +418,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
val items = withContext(Dispatchers.IO){
|
||||
downloadViewModel.getProcessingDownloads()
|
||||
}
|
||||
val flatFormatCollection = items.map { it.allFormats }.flatten()
|
||||
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)
|
||||
val bottomSheet = FormatSelectionBottomSheetDialog(items, _multipleFormatsListener = formatListener)
|
||||
bottomSheet.show(parentFragmentManager, "formatSheet")
|
||||
}
|
||||
}
|
||||
|
|
@ -661,10 +645,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading }
|
||||
}
|
||||
|
||||
private fun toggleLoadingShimmerTitle(show: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
private fun updateFileSize(items: List<Long>){
|
||||
if (items.all { it > 5L }){
|
||||
val size = FileUtil.convertFileSize(items.sum())
|
||||
|
|
@ -822,12 +802,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
override fun onDismiss(dialog: DialogInterface) {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
getProcessingItemsData()?.apply {
|
||||
if (this.job?.isActive == true){
|
||||
this.job?.cancel(CancellationException())
|
||||
downloadViewModel.deleteAllWithID(this.processingDownloadItemIDs)
|
||||
}
|
||||
}
|
||||
downloadViewModel.processingItemsJob?.cancel(CancellationException())
|
||||
downloadViewModel.processingItemsJob = null
|
||||
downloadViewModel.deleteProcessing()
|
||||
}
|
||||
}
|
||||
|
|
@ -912,8 +888,5 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
}
|
||||
}
|
||||
|
||||
private fun getProcessingItemsData() : DownloadViewModel.ProcessingItemsJob? {
|
||||
return downloadViewModel.processingItemsFlow
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,9 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
private lateinit var saveDir : TextInputLayout
|
||||
private lateinit var freeSpace : TextView
|
||||
private lateinit var infoUtil: InfoUtil
|
||||
|
||||
private lateinit var genericVideoFormats: MutableList<Format>
|
||||
private lateinit var genericAudioFormats: MutableList<Format>
|
||||
|
||||
lateinit var downloadItem: DownloadItem
|
||||
|
||||
|
|
@ -77,6 +79,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
|
||||
infoUtil = InfoUtil(requireContext())
|
||||
genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources)
|
||||
genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources)
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
shownFields = preferences.getStringSet("modify_download_card", requireContext().getStringArray(R.array.modify_download_card_values).toSet())!!.toList()
|
||||
return fragmentView
|
||||
|
|
@ -234,34 +237,38 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
val chosenFormat = downloadItem.format
|
||||
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) })
|
||||
val listener = object : OnFormatClickListener {
|
||||
override fun onFormatClick(item: List<FormatTuple>) {
|
||||
item.first().format?.apply {
|
||||
override fun onFormatClick(formatTuple: FormatTuple) {
|
||||
formatTuple.format?.apply {
|
||||
downloadItem.format = this
|
||||
}
|
||||
downloadItem.videoPreferences.audioFormatIDs.clear()
|
||||
item.first().audioFormats?.map { it.format_id }?.let {
|
||||
formatTuple.audioFormats?.map { it.format_id }?.let {
|
||||
downloadItem.videoPreferences.audioFormatIDs.addAll(it)
|
||||
}
|
||||
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 {
|
||||
withContext(Dispatchers.IO){
|
||||
resultItem?.apply {
|
||||
this.formats.removeAll(formats)
|
||||
this.formats.addAll(allFormats.first().filter { !genericVideoFormats.contains(it) })
|
||||
this.formats.addAll(allFormats.filter { !genericVideoFormats.contains(it) })
|
||||
resultViewModel.update(this)
|
||||
kotlin.runCatching {
|
||||
val f1 = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
|
||||
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 preferredAudioFormats = downloadViewModel.getPreferredAudioFormats(formats)
|
||||
downloadItem.format = preferredFormat
|
||||
|
|
@ -274,7 +281,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
}
|
||||
formatCard.setOnClickListener{
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
@ -413,6 +420,10 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
|
|||
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun updateSelectedAudioFormat(format: Format){
|
||||
if (genericAudioFormats.contains(format)) {
|
||||
return
|
||||
}
|
||||
|
||||
downloadItem.videoPreferences.audioFormatIDs.clear()
|
||||
downloadItem.videoPreferences.audioFormatIDs.addAll(arrayListOf(format.format_id))
|
||||
val formatCard = requireView().findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ import com.deniscerri.ytdl.R
|
|||
import com.deniscerri.ytdl.database.models.AlreadyExistsItem
|
||||
import com.deniscerri.ytdl.database.models.DownloadItem
|
||||
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.ResultViewModel
|
||||
import com.deniscerri.ytdl.database.viewmodel.SharedDownloadViewModel.AlreadyExistsIDs
|
||||
import com.deniscerri.ytdl.ui.adapter.AlreadyExistsAdapter
|
||||
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
|
||||
import com.deniscerri.ytdl.util.UiUtil
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import android.os.Bundle
|
|||
import android.util.DisplayMetrics
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.*
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.forEach
|
||||
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.Type
|
||||
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.InfoUtil
|
||||
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.chip.Chip
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.internal.format
|
||||
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 infoUtil: InfoUtil
|
||||
private lateinit var view: View
|
||||
private lateinit var continueInBackgroundSnackBar : Snackbar
|
||||
private lateinit var downloadViewModel: DownloadViewModel
|
||||
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 audioFormatList : LinearLayout
|
||||
private lateinit var okBtn : Button
|
||||
private lateinit var refreshBtn: Button
|
||||
private lateinit var videoTitle : 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 filterBy : FormatCategory
|
||||
private lateinit var filterBtn : Button
|
||||
|
||||
private lateinit var continueInBackgroundSnackBar : Snackbar
|
||||
private lateinit var view: View
|
||||
private var updateFormatsJob: Job? = null
|
||||
private var isMissingFormats: Boolean = false
|
||||
|
||||
private var hasGenericFormats: Boolean = false
|
||||
|
||||
private lateinit var items: List<DownloadItem?>
|
||||
private lateinit var formats: List<List<Format>>
|
||||
private lateinit var items: MutableList<DownloadItem?>
|
||||
private lateinit var formats: MutableList<Format>
|
||||
private lateinit var listener: OnFormatClickListener
|
||||
private lateinit var multipleFormatsListener: OnMultipleFormatClickListener
|
||||
|
||||
private var currentFormatSource : String? = null
|
||||
|
||||
private lateinit var genericAudioFormats : List<Format>
|
||||
private lateinit var genericVideoFormats : List<Format>
|
||||
|
||||
enum class FormatSorting {
|
||||
filesize, container, codec, id
|
||||
}
|
||||
|
|
@ -82,7 +96,6 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
infoUtil = InfoUtil(requireActivity().applicationContext)
|
||||
formatCollection = mutableListOf()
|
||||
chosenFormats = listOf()
|
||||
selectedAudios = mutableListOf()
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
|
@ -101,9 +114,27 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
return
|
||||
}
|
||||
|
||||
items = _items
|
||||
formats = _formats!!
|
||||
listener = _listener!!
|
||||
items = _items.distinctBy { it!!.url }.toMutableList()
|
||||
if (items.size == 1) {
|
||||
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")!!)
|
||||
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)
|
||||
|
||||
shimmers.visibility = View.GONE
|
||||
hasGenericFormats = formats.first().isEmpty() || formats.last().any { it.format_id == "best" || it.format_id == "ba" }
|
||||
filterBtn.isVisible = !hasGenericFormats
|
||||
isMissingFormats = formats.isEmpty() && items.any { it!!.allFormats.isEmpty() }
|
||||
|
||||
if (items.size > 1){
|
||||
|
||||
if (!hasGenericFormats){
|
||||
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()}
|
||||
if (!isMissingFormats){
|
||||
chosenFormats = formats.mapTo(mutableListOf()) {it.copy()}
|
||||
chosenFormats = when(items.first()?.type){
|
||||
Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
|
||||
else -> chosenFormats
|
||||
}
|
||||
chosenFormats.forEach {
|
||||
it.filesize =
|
||||
flattenFormats.filter { f -> f.format_id == it.format_id }
|
||||
.sumOf { itt -> itt.filesize }
|
||||
it.filesize = items.map { itm -> itm!!.allFormats }.flatten().filter { f -> f.format_id == it.format_id }.sumOf { itt -> itt.filesize }
|
||||
}
|
||||
}else{
|
||||
chosenFormats = formats.flatten()
|
||||
chosenFormats = formats
|
||||
}
|
||||
addFormatsToView()
|
||||
}else{
|
||||
chosenFormats = formats.flatten()
|
||||
if(!hasGenericFormats){
|
||||
chosenFormats = formats
|
||||
if(!isMissingFormats){
|
||||
if(items.first()?.type == Type.audio){
|
||||
chosenFormats = chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
|
||||
}
|
||||
|
|
@ -159,41 +183,22 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
addFormatsToView()
|
||||
}
|
||||
|
||||
val refreshBtn = view.findViewById<Button>(R.id.format_refresh)
|
||||
if (!hasGenericFormats || items.isEmpty() || items.first()?.url?.isEmpty() == true) refreshBtn.visibility = View.GONE
|
||||
|
||||
val formatSourceLinear = view.findViewById<MaterialCardView>(R.id.formatSourceLinear)
|
||||
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 = view.findViewById(R.id.format_refresh)
|
||||
filterBtn.isVisible = chosenFormats.isNotEmpty() || items.all { it!!.url.isYoutubeURL() }
|
||||
if (!isMissingFormats || items.isEmpty() || items.first()?.url?.isEmpty() == true) {
|
||||
refreshBtn.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
||||
refreshBtn.setOnClickListener {
|
||||
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.setAction(R.string.ok) {
|
||||
listener.onContinueOnBackground()
|
||||
_multipleFormatsListener!!.onContinueOnBackground()
|
||||
this@FormatSelectionBottomSheetDialog.dismiss()
|
||||
}
|
||||
continueInBackgroundSnackBar.show()
|
||||
|
|
@ -202,19 +207,19 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
|
||||
chosenFormats = emptyList()
|
||||
refreshBtn.isEnabled = false
|
||||
formatSourceLinear.isVisible = false
|
||||
refreshBtn.isVisible = true
|
||||
okBtn.isVisible = false
|
||||
okBtn.isEnabled = false
|
||||
filterBtn.isEnabled = false
|
||||
formatListLinearLayout.visibility = View.GONE
|
||||
shimmers.visibility = View.VISIBLE
|
||||
shimmers.startShimmer()
|
||||
updateFormatsJob = launch {
|
||||
updateFormatsJob = launch(Dispatchers.IO) {
|
||||
try{
|
||||
//simple download
|
||||
if (items.size == 1) {
|
||||
formatCollection.clear()
|
||||
kotlin.runCatching {
|
||||
val res = withContext(Dispatchers.IO){
|
||||
infoUtil.getFormats(items.first()!!.url, currentFormatSource)
|
||||
}
|
||||
val res = infoUtil.getFormats(items.first()!!.url, currentFormatSource)
|
||||
res.filter { it.format_note != "storyboard" }
|
||||
chosenFormats = if (items.first()?.type == Type.audio) {
|
||||
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()
|
||||
|
||||
formats = listOf(res)
|
||||
formats.clear()
|
||||
formats.addAll(res)
|
||||
withContext(Dispatchers.Main){
|
||||
listener.onFormatsUpdated(formats)
|
||||
listener.onFormatsUpdated(res)
|
||||
}
|
||||
formatCollection.add(res)
|
||||
}.onFailure { err ->
|
||||
withContext(Dispatchers.Main){
|
||||
UiUtil.handleResultResponse(requireActivity(), ResultViewModel.ResultsUiState(
|
||||
|
|
@ -240,26 +245,44 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
|
||||
//list format filtering
|
||||
}else{
|
||||
var progress = "0/${items.size}"
|
||||
formatCollection.clear()
|
||||
formats.clear()
|
||||
var progressInt = 0
|
||||
val formatCollection = itemsThatHaveFormats.map { it!!.allFormats }.toMutableList()
|
||||
|
||||
var progress = "0/${itemsWithMissingFormats.size}"
|
||||
withContext(Dispatchers.Main) {
|
||||
refreshBtn.text = progress
|
||||
}
|
||||
withContext(Dispatchers.IO){
|
||||
infoUtil.getFormatsMultiple(items.map { it!!.url }, currentFormatSource) {
|
||||
if (isActive) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
formatCollection.add(it)
|
||||
progress = "${formatCollection.size}/${items.size}"
|
||||
refreshBtn.text = progress
|
||||
|
||||
val res = infoUtil.getFormatsMultiple(itemsWithMissingFormats.map { it!!.url }, currentFormatSource) {
|
||||
if (!isActive) return@getFormatsMultiple
|
||||
|
||||
if (it.unavailable) {
|
||||
lifecycleScope.launch {
|
||||
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){
|
||||
listener.onFormatsUpdated(formats)
|
||||
}
|
||||
|
||||
formatCollection.addAll(res)
|
||||
|
||||
if (!isActive) return@launch
|
||||
|
||||
val flatFormatCollection = formatCollection.flatten()
|
||||
val commonFormats =
|
||||
|
|
@ -267,14 +290,13 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
.filter { it.value == items.size }
|
||||
.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }
|
||||
.map { it.value }
|
||||
formats.addAll(commonFormats)
|
||||
|
||||
chosenFormats = commonFormats.filter { it.filesize != 0L }
|
||||
.mapTo(mutableListOf()) { it.copy() }
|
||||
chosenFormats = when (items.first()?.type) {
|
||||
Type.audio -> chosenFormats.filter {
|
||||
it.format_note.contains(
|
||||
"audio",
|
||||
ignoreCase = true
|
||||
)
|
||||
it.vcodec.isBlank() || it.vcodec == "none"
|
||||
}
|
||||
|
||||
else -> chosenFormats
|
||||
|
|
@ -286,24 +308,25 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
.sumOf { itt -> itt.filesize }
|
||||
}
|
||||
}
|
||||
hasGenericFormats = formatCollection.size == 0
|
||||
isMissingFormats = formats.isEmpty()
|
||||
withContext(Dispatchers.Main){
|
||||
shimmers.visibility = View.GONE
|
||||
shimmers.stopShimmer()
|
||||
formatSourceLinear.isVisible = canSwitch
|
||||
filterBtn.isVisible = !hasGenericFormats
|
||||
addFormatsToView()
|
||||
refreshBtn.isVisible = hasGenericFormats
|
||||
refreshBtn.isEnabled = hasGenericFormats
|
||||
refreshBtn.isVisible = isMissingFormats
|
||||
refreshBtn.isEnabled = isMissingFormats
|
||||
filterBtn.isEnabled = true
|
||||
okBtn.isEnabled = true
|
||||
formatListLinearLayout.visibility = View.VISIBLE
|
||||
}
|
||||
}catch (e: Exception){
|
||||
withContext(Dispatchers.Main) {
|
||||
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
|
||||
shimmers.visibility = View.GONE
|
||||
formatSourceLinear.isVisible = canSwitch
|
||||
shimmers.stopShimmer()
|
||||
|
||||
e.printStackTrace()
|
||||
|
|
@ -334,54 +357,92 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
filterSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
filterSheet.setContentView(R.layout.format_category_sheet)
|
||||
|
||||
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)
|
||||
//format filter
|
||||
filterSheet.findViewById<LinearLayout>(R.id.format_filter_linear)?.isVisible = !isMissingFormats
|
||||
if (!isMissingFormats) {
|
||||
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!!)
|
||||
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
|
||||
when(filterBy) {
|
||||
FormatCategory.ALL -> all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
|
||||
FormatCategory.SUGGESTED -> {
|
||||
val filterOptions = listOf(all!!, suggested!!,smallest!!, generic!!)
|
||||
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
|
||||
when(filterBy) {
|
||||
FormatCategory.ALL -> all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
addFormatsToView()
|
||||
filterSheet.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
addFormatsToView()
|
||||
filterSheet.dismiss()
|
||||
}
|
||||
smallest.setOnClickListener {
|
||||
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
|
||||
filterBy = FormatCategory.SMALLEST
|
||||
smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
|
||||
addFormatsToView()
|
||||
filterSheet.dismiss()
|
||||
}
|
||||
generic.setOnClickListener {
|
||||
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
|
||||
filterBy = FormatCategory.GENERIC
|
||||
generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
|
||||
addFormatsToView()
|
||||
filterSheet.dismiss()
|
||||
//format source
|
||||
val formatSourceLinear = filterSheet.findViewById<LinearLayout>(R.id.format_source_linear)!!
|
||||
val canSwitch = items.all { it!!.url.isYoutubeURL() }
|
||||
formatSourceLinear.isVisible = canSwitch
|
||||
if (canSwitch) {
|
||||
val formatSourceOptions = mutableListOf<TextView>()
|
||||
|
||||
val availableSources = resources.getStringArray(R.array.formats_source)
|
||||
val availableSourcesValues = resources.getStringArray(R.array.formats_source_values)
|
||||
val currentSource = currentFormatSource ?: sharedPreferences.getString("formats_source", "yt-dlp")
|
||||
formatSourceLinear.isVisible = true
|
||||
availableSources.forEachIndexed { idx, it ->
|
||||
val txt = requireActivity().layoutInflater.inflate(R.layout.selectable_textview_filter, null) as TextView
|
||||
val tag = availableSourcesValues[idx]
|
||||
|
||||
txt.text = it
|
||||
txt.tag = tag
|
||||
txt.setOnClickListener {
|
||||
currentFormatSource = it.tag.toString()
|
||||
refreshBtn.performClick()
|
||||
filterSheet.dismiss()
|
||||
}
|
||||
formatSourceOptions.add(txt)
|
||||
formatSourceLinear.addView(txt)
|
||||
}
|
||||
formatSourceOptions.firstOrNull { it.tag == currentSource }?.apply {
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
val displayMetrics = DisplayMetrics()
|
||||
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
filterSheet.behavior.peekHeight = displayMetrics.heightPixels
|
||||
|
|
@ -390,21 +451,26 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
}
|
||||
|
||||
private fun returnFormats(){
|
||||
//simple video format selection
|
||||
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{
|
||||
//playlist format selection
|
||||
val selectedFormats = mutableListOf<Format?>()
|
||||
formatCollection.forEach {
|
||||
selectedFormats.add(it.first{ f -> f.format_id == selectedVideo?.format_id})
|
||||
val formatsToReturn = mutableListOf<MultipleItemFormatTuple>()
|
||||
items.forEach {
|
||||
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()) {
|
||||
items.forEach { _ ->
|
||||
selectedFormats.add(selectedVideo)
|
||||
}
|
||||
}
|
||||
listener.onFormatClick(selectedFormats.map { FormatTuple(it, selectedAudios) })
|
||||
multipleFormatsListener.onFormatClick(formatsToReturn)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
audioFormatList.removeAllViews()
|
||||
|
||||
|
|
@ -495,9 +563,9 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
|
||||
if (finalFormats.isEmpty()){
|
||||
finalFormats = if (items.first()?.type == Type.audio){
|
||||
infoUtil.getGenericAudioFormats(requireContext().resources)
|
||||
genericAudioFormats
|
||||
}else{
|
||||
infoUtil.getGenericVideoFormats(requireContext().resources)
|
||||
genericVideoFormats
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -533,18 +601,22 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
}
|
||||
}else{
|
||||
if (items.size == 1){
|
||||
listener.onFormatClick(listOf(FormatTuple(format, null)))
|
||||
listener.onFormatClick(FormatTuple(format, null))
|
||||
}else{
|
||||
val selectedFormats = mutableListOf<Format>()
|
||||
formatCollection.forEach {
|
||||
selectedFormats.add(it.firstOrNull{ f -> f.format_id == format.format_id} ?: format)
|
||||
val formatsToReturn = mutableListOf<MultipleItemFormatTuple>()
|
||||
val f = if (genericAudioFormats.contains(format) || genericVideoFormats.contains(format)) format else null
|
||||
items.forEach {
|
||||
formatsToReturn.add(
|
||||
MultipleItemFormatTuple(
|
||||
it!!.url,
|
||||
FormatTuple(
|
||||
f ?: it.allFormats.firstOrNull { af -> af.format_id == format.format_id },
|
||||
null
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
if (selectedFormats.isEmpty()) {
|
||||
items.forEach { _ ->
|
||||
selectedFormats.add(format)
|
||||
}
|
||||
}
|
||||
listener.onFormatClick(selectedFormats.map { FormatTuple(it, null) })
|
||||
multipleFormatsListener.onFormatClick(formatsToReturn)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
|
@ -594,25 +666,36 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
cleanUp()
|
||||
super.onDismiss(dialog)
|
||||
}
|
||||
|
||||
|
||||
private fun cleanUp(){
|
||||
kotlin.runCatching {
|
||||
updateFormatsJob?.cancel()
|
||||
updateFormatsJob?.cancel(CancellationException())
|
||||
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("formatSheet")!!).commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OnFormatClickListener{
|
||||
fun onFormatClick(selectedFormats: List<FormatTuple>)
|
||||
fun onContinueOnBackground() {}
|
||||
fun onFormatsUpdated(allFormats: List<List<Format>>)
|
||||
fun onFormatClick(formatTuple: FormatTuple)
|
||||
fun onFormatsUpdated(allFormats: 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(
|
||||
var format: Format?,
|
||||
var audioFormats: List<Format>?
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import android.widget.TextView
|
|||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.MediaItem
|
||||
|
|
@ -39,11 +40,11 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.work.WorkManager
|
||||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.database.models.DownloadItem
|
||||
import com.deniscerri.ytdl.database.models.ResultItem
|
||||
import com.deniscerri.ytdl.database.repository.DownloadRepository
|
||||
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
|
||||
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.GenericDownloadAdapter
|
||||
import com.deniscerri.ytdl.util.Extensions.setFullScreen
|
||||
|
|
@ -452,127 +453,28 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
|
|||
downloadViewModel.getItemByID(itemID)
|
||||
}
|
||||
|
||||
val bottomSheet = BottomSheetDialog(requireContext())
|
||||
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet)
|
||||
val title = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)
|
||||
title!!.text = item.title.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
|
||||
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
|
||||
author!!.text = item.author.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
|
||||
|
||||
// BUTTON ----------------------------------
|
||||
val btn = bottomSheet.findViewById<MaterialButton>(R.id.downloads_download_button_type)
|
||||
|
||||
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))
|
||||
}
|
||||
UiUtil.showDownloadItemDetailsCard(
|
||||
item,
|
||||
requireActivity(),
|
||||
DownloadRepository.Status.valueOf(item.status),
|
||||
removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
|
||||
sheet.hide()
|
||||
removeQueuedItem(itemID)
|
||||
},
|
||||
downloadItem = {
|
||||
runBlocking{
|
||||
downloadViewModel.queueDownloads(listOf(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
|
||||
View.GONE
|
||||
else formatNote!!.text = item.format.format_note
|
||||
|
||||
if (item.format.container != "") container!!.text = item.format.container.uppercase()
|
||||
else container!!.visibility = View.GONE
|
||||
|
||||
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
|
||||
},
|
||||
longClickDownloadButton = {
|
||||
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
|
||||
Pair("downloadItem", it),
|
||||
Pair("result", downloadViewModel.createResultItemFromDownload(it)),
|
||||
Pair("type", it.type)
|
||||
)
|
||||
)
|
||||
},
|
||||
scheduleButtonClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -608,7 +510,6 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
|
|||
lifecycleScope.launch {
|
||||
val id = itemID.toInt()
|
||||
YoutubeDL.getInstance().destroyProcessById(id.toString())
|
||||
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString())
|
||||
notificationUtil.cancelDownloadNotification(id)
|
||||
|
||||
withContext(Dispatchers.IO){
|
||||
|
|
|
|||
|
|
@ -15,14 +15,19 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ListView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.os.bundleOf
|
||||
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.lifecycle.ViewModelProvider
|
||||
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.BadgeUtils
|
||||
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.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
|
|
@ -70,27 +76,15 @@ import org.greenrobot.eventbus.Subscribe
|
|||
import org.greenrobot.eventbus.ThreadMode
|
||||
|
||||
|
||||
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener, QueuedDownloadAdapter.OnItemClickListener {
|
||||
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener {
|
||||
private var fragmentView: View? = null
|
||||
private var activity: Activity? = null
|
||||
private lateinit var downloadViewModel : DownloadViewModel
|
||||
|
||||
private lateinit var activeDownloadsLinear: LinearLayout
|
||||
private lateinit var activeDownloadsNoResults: View
|
||||
//private lateinit var activeDownloadsLinear: LinearLayout
|
||||
private lateinit var activeRecyclerView : RecyclerView
|
||||
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 pause: ExtendedFloatingActionButton
|
||||
private lateinit var resume: ExtendedFloatingActionButton
|
||||
|
|
@ -119,34 +113,12 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
|
|||
workManager = WorkManager.getInstance(requireContext())
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
||||
activeDownloadsLinear = view.findViewById(R.id.active_linear)
|
||||
activeDownloadsNoResults = view.findViewById(R.id.active_no_results)
|
||||
activeDownloads = ActiveDownloadAdapter(this,requireActivity())
|
||||
activeRecyclerView = view.findViewById(R.id.download_recyclerview)
|
||||
activeRecyclerView.forceFastScrollMode()
|
||||
activeRecyclerView.adapter = activeDownloads
|
||||
activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
|
||||
|
||||
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.isVisible = false
|
||||
resume = view.findViewById(R.id.resume)
|
||||
|
|
@ -164,13 +136,13 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
|
|||
notificationUtil.cancelDownloadNotification(it.id.toInt())
|
||||
}
|
||||
preferences.edit().putBoolean("paused_downloads", true).apply()
|
||||
pause.isEnabled = false
|
||||
pause.isVisible = false
|
||||
resume.isEnabled = false
|
||||
resume.isVisible = true
|
||||
activeDownloads.notifyDataSetChanged()
|
||||
withContext(Dispatchers.Main){
|
||||
delay(1000)
|
||||
pause.isVisible = false
|
||||
pause.isEnabled = true
|
||||
resume.isVisible = true
|
||||
resume.isEnabled = true
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -179,7 +151,9 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
|
|||
resume.setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
preferences.edit().putBoolean("paused_downloads", false).apply()
|
||||
resume.isEnabled = false
|
||||
resume.isVisible = false
|
||||
pause.isEnabled = false
|
||||
pause.isVisible = true
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadViewModel.resetActiveToQueued()
|
||||
downloadViewModel.startDownloadWorker(listOf())
|
||||
|
|
@ -189,16 +163,14 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
|
|||
}
|
||||
withContext(Dispatchers.Main){
|
||||
delay(1000)
|
||||
resume.isVisible = false
|
||||
resume.isEnabled = true
|
||||
pause.isVisible = true
|
||||
pause.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
val activeQueuedDownloadsCount = withContext(Dispatchers.IO) {
|
||||
downloadViewModel.getActiveDownloadsCount()
|
||||
downloadViewModel.getActiveQueuedDownloadsCount()
|
||||
}
|
||||
val pausedDownloads = preferences.getBoolean("paused_downloads", false)
|
||||
pause.isVisible = activeQueuedDownloadsCount > 0 && !pausedDownloads
|
||||
|
|
@ -207,8 +179,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
|
|||
|
||||
lifecycleScope.launch {
|
||||
downloadViewModel.activeQueuedDownloadsCount.collectLatest {
|
||||
activeQueuedLinear.isVisible = it > 0
|
||||
noResults.isVisible = it == 0
|
||||
if (it == 0) {
|
||||
pause.isVisible = false
|
||||
resume.isVisible = false
|
||||
|
|
@ -218,57 +188,11 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
|
|||
|
||||
lifecycleScope.launch {
|
||||
downloadViewModel.activeDownloads.collectLatest {
|
||||
activeDownloadsNoResults.isVisible = it.isEmpty()
|
||||
noResults.isVisible = it.isEmpty()
|
||||
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
|
||||
|
|
@ -280,14 +204,14 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
|
|||
requireActivity().runOnUiThread {
|
||||
try {
|
||||
progressBar?.setProgressCompat(event.progress, true)
|
||||
if (event.progress > 95) {
|
||||
pause.isEnabled = false
|
||||
}
|
||||
outputText?.text = event.output
|
||||
}catch (ignored: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
fun hidePauseBtn(){
|
||||
pause.isVisible = false
|
||||
}
|
||||
|
||||
override fun 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -79,6 +79,8 @@ class DownloadQueueMainFragment : Fragment(){
|
|||
val isInNavBar = NavbarUtil.getNavBarItems(requireActivity()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
|
||||
if (isInNavBar) {
|
||||
topAppBar.navigationIcon = null
|
||||
}else{
|
||||
mainActivity.hideBottomNavigation()
|
||||
}
|
||||
topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() }
|
||||
|
||||
|
|
@ -90,7 +92,7 @@ class DownloadQueueMainFragment : Fragment(){
|
|||
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(
|
||||
childFragmentManager,
|
||||
|
|
@ -104,11 +106,11 @@ class DownloadQueueMainFragment : Fragment(){
|
|||
TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
|
||||
when (position) {
|
||||
0 -> tab.text = getString(R.string.running)
|
||||
// 1 -> tab.text = getString(R.string.in_queue)
|
||||
1 -> tab.text = getString(R.string.scheduled)
|
||||
2 -> tab.text = getString(R.string.cancelled)
|
||||
3 -> tab.text = getString(R.string.errored)
|
||||
4 -> tab.text = getString(R.string.saved)
|
||||
1 -> tab.text = getString(R.string.in_queue)
|
||||
2 -> tab.text = getString(R.string.scheduled)
|
||||
3 -> tab.text = getString(R.string.cancelled)
|
||||
4 -> tab.text = getString(R.string.errored)
|
||||
5 -> tab.text = getString(R.string.saved)
|
||||
}
|
||||
}.attach()
|
||||
|
||||
|
|
@ -130,13 +132,12 @@ class DownloadQueueMainFragment : Fragment(){
|
|||
initMenu()
|
||||
}
|
||||
})
|
||||
mainActivity.hideBottomNavigation()
|
||||
initMenu()
|
||||
|
||||
if (arguments?.getString("tab") != null){
|
||||
tabLayout.getTabAt(3)!!.select()
|
||||
tabLayout.getTabAt(4)!!.select()
|
||||
viewPager2.postDelayed( {
|
||||
viewPager2.setCurrentItem(3, false)
|
||||
viewPager2.setCurrentItem(4, false)
|
||||
val reconfigureID = arguments?.getLong("reconfigure")
|
||||
reconfigureID?.apply {
|
||||
notificationUtil.cancelErroredNotification(this.toInt())
|
||||
|
|
@ -166,30 +167,30 @@ class DownloadQueueMainFragment : Fragment(){
|
|||
}
|
||||
}
|
||||
}
|
||||
// lifecycleScope.launch {
|
||||
// downloadViewModel.queuedDownloadsCount.collectLatest {
|
||||
// tabLayout.getTabAt(1)?.apply {
|
||||
// createBadge(it)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
lifecycleScope.launch {
|
||||
downloadViewModel.scheduledDownloadsCount.collectLatest {
|
||||
downloadViewModel.queuedDownloadsCount.collectLatest {
|
||||
tabLayout.getTabAt(1)?.apply {
|
||||
createBadge(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
downloadViewModel.cancelledDownloadsCount.collectLatest {
|
||||
downloadViewModel.scheduledDownloadsCount.collectLatest {
|
||||
tabLayout.getTabAt(2)?.apply {
|
||||
createBadge(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
downloadViewModel.erroredDownloadsCount.collectLatest {
|
||||
downloadViewModel.cancelledDownloadsCount.collectLatest {
|
||||
tabLayout.getTabAt(3)?.apply {
|
||||
createBadge(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
downloadViewModel.erroredDownloadsCount.collectLatest {
|
||||
tabLayout.getTabAt(4)?.apply {
|
||||
removeBadge()
|
||||
if (it > 0) createBadge(it)
|
||||
}
|
||||
|
|
@ -197,7 +198,7 @@ class DownloadQueueMainFragment : Fragment(){
|
|||
}
|
||||
lifecycleScope.launch {
|
||||
downloadViewModel.savedDownloadsCount.collectLatest {
|
||||
tabLayout.getTabAt(4)?.apply {
|
||||
tabLayout.getTabAt(5)?.apply {
|
||||
removeBadge()
|
||||
if (it > 0) createBadge(it)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,14 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
|
|||
sortChip = view.findViewById(R.id.sortChip)
|
||||
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()
|
||||
allhistoryList = mutableListOf()
|
||||
|
|
|
|||
|
|
@ -211,7 +211,6 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
|
|||
adapter.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)
|
||||
|
|
@ -222,20 +221,6 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
|
|||
deleteDialog.show()
|
||||
true
|
||||
}
|
||||
R.id.download -> {
|
||||
lifecycleScope.launch {
|
||||
val selectedObjects = getSelectedIDs()
|
||||
adapter.clearCheckedItems()
|
||||
for (id in selectedObjects){
|
||||
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(selectedObjects)
|
||||
}
|
||||
actionMode?.finish()
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.select_all -> {
|
||||
adapter.checkAll()
|
||||
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
|
||||
|
|
@ -469,7 +454,6 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
|
|||
downloadItem = {
|
||||
downloadViewModel.deleteDownload(it.id)
|
||||
it.downloadStartTime = 0
|
||||
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
|
||||
runBlocking {
|
||||
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()
|
||||
downloadViewModel.deleteDownload(downloadItem.id)
|
||||
downloadItem.downloadStartTime = it.timeInMillis
|
||||
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(listOf(downloadItem))
|
||||
}
|
||||
|
|
@ -507,16 +490,11 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
|
|||
lifecycleScope.launch {
|
||||
val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
|
||||
if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
|
||||
val now = System.currentTimeMillis()
|
||||
actionMode?.apply {
|
||||
if (selectedObjects == 0){
|
||||
this.finish()
|
||||
}else{
|
||||
this.title = "$selectedObjects ${getString(R.string.selected)}"
|
||||
this.menu.findItem(R.id.download).isVisible = withContext(Dispatchers.IO){
|
||||
downloadViewModel.checkAllQueuedItemsAreScheduledAfterNow(adapter.checkedItems.toList(), adapter.inverted, now)
|
||||
}
|
||||
|
||||
this.menu.findItem(R.id.up).isVisible = position > 0
|
||||
this.menu.findItem(R.id.down).isVisible = position < totalSize
|
||||
this.menu.findItem(R.id.select_between).isVisible = false
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import com.deniscerri.ytdl.util.Extensions.enableFastScroll
|
|||
import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode
|
||||
import com.deniscerri.ytdl.util.Extensions.toListString
|
||||
import com.deniscerri.ytdl.util.UiUtil
|
||||
import com.deniscerri.ytdl.work.AlarmScheduler
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
|
@ -138,7 +139,6 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
|
|||
downloadItem = {
|
||||
downloadViewModel.deleteDownload(it.id)
|
||||
it.downloadStartTime = 0
|
||||
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
|
||||
runBlocking {
|
||||
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()
|
||||
downloadViewModel.deleteDownload(downloadItem.id)
|
||||
downloadItem.downloadStartTime = it.timeInMillis
|
||||
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(listOf(downloadItem))
|
||||
adapter.notifyItemChanged(position)
|
||||
|
|
@ -269,9 +268,6 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
|
|||
lifecycleScope.launch {
|
||||
val selectedObjects = getSelectedIDs()
|
||||
adapter.clearCheckedItems()
|
||||
for (id in selectedObjects){
|
||||
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(selectedObjects)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class MoreFragment : Fragment() {
|
|||
private lateinit var logs: TextView
|
||||
private lateinit var commandTemplates: TextView
|
||||
private lateinit var downloadQueue: TextView
|
||||
private lateinit var downloads: TextView
|
||||
private lateinit var cookies: TextView
|
||||
private lateinit var observeSources: TextView
|
||||
private lateinit var terminateApp: TextView
|
||||
|
|
@ -59,14 +60,26 @@ class MoreFragment : Fragment() {
|
|||
terminal = view.findViewById(R.id.terminal)
|
||||
logs = view.findViewById(R.id.logs)
|
||||
commandTemplates = view.findViewById(R.id.command_templates)
|
||||
downloads = view.findViewById(R.id.downloads)
|
||||
downloadQueue = view.findViewById(R.id.download_queue)
|
||||
cookies = view.findViewById(R.id.cookies)
|
||||
observeSources = view.findViewById(R.id.observe_sources)
|
||||
terminateApp = view.findViewById(R.id.terminate)
|
||||
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
|
||||
downloads.isVisible = !showingDownloads
|
||||
downloadQueue.isVisible = !showingDownloadQueue
|
||||
|
||||
terminal.setOnClickListener {
|
||||
val intent = Intent(context, TerminalActivity::class.java)
|
||||
|
|
@ -81,8 +94,9 @@ class MoreFragment : Fragment() {
|
|||
findNavController().navigate(R.id.commandTemplatesFragment)
|
||||
}
|
||||
|
||||
val showingDownloadQueue = NavbarUtil.getNavBarItems(requireContext()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
|
||||
downloadQueue.isVisible = !showingDownloadQueue
|
||||
downloads.setOnClickListener {
|
||||
findNavController().navigate(R.id.historyFragment)
|
||||
}
|
||||
|
||||
downloadQueue.setOnClickListener {
|
||||
findNavController().navigate(R.id.downloadQueueMainFragment)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import com.google.android.material.snackbar.Snackbar
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
class DownloadLogFragment : Fragment() {
|
||||
|
|
@ -96,7 +97,9 @@ class DownloadLogFragment : Fragment() {
|
|||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val logItem = logViewModel.getItemById(id!!)
|
||||
topAppBar.title = logItem.title
|
||||
withContext(Dispatchers.Main){
|
||||
topAppBar.title = logItem.title
|
||||
}
|
||||
}
|
||||
|
||||
content.isFocusable = true
|
||||
|
|
|
|||
|
|
@ -69,13 +69,29 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
|
|||
archivePathResultLauncher.launch(intent)
|
||||
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 scheduleStart = findPreference<Preference>("schedule_start")
|
||||
scheduleStart?.summary = preferences.getString("schedule_start", "00:00")
|
||||
val scheduleEnd = findPreference<Preference>("schedule_end")
|
||||
scheduleEnd?.summary = preferences.getString("schedule_end", "05:00")
|
||||
val scheduler = AlarmScheduler(requireContext())
|
||||
|
||||
useScheduler?.setOnPreferenceChangeListener { preference, newValue ->
|
||||
var allowChange = true
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.deniscerri.ytdl.App
|
||||
import com.deniscerri.ytdl.R
|
||||
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.repository.DownloadRepository
|
||||
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory
|
||||
|
|
@ -476,4 +477,8 @@ object Extensions {
|
|||
"<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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -57,33 +57,53 @@ class FormatSorter(private var context: Context) {
|
|||
fun sortAudioFormats(formats: List<Format>) : List<Format> {
|
||||
val orderPreferences = getAudioFormatImportance()
|
||||
|
||||
val fieldSorter: Comparator<Format> = Comparator { a, b ->
|
||||
for (order in orderPreferences) {
|
||||
when(order) {
|
||||
"smallsize" -> {
|
||||
(a.filesize).compareTo(b.filesize)
|
||||
}
|
||||
"id" -> {
|
||||
(audioFormatIDPreference.contains(b.format_id)).compareTo(audioFormatIDPreference.contains(a.format_id))
|
||||
}
|
||||
"language" -> {
|
||||
if (audioLanguagePreference.isBlank()) {
|
||||
0
|
||||
val fieldSorter: Comparator<Format> = object : Comparator<Format> {
|
||||
override fun compare(a: Format, b: Format): Int {
|
||||
for (order in orderPreferences) {
|
||||
val comparison = when (order) {
|
||||
"smallsize" -> {
|
||||
(a.filesize).compareTo(b.filesize)
|
||||
}
|
||||
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" -> {
|
||||
("^(${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)
|
||||
}
|
||||
if (comparison != 0) return comparison
|
||||
}
|
||||
return 0
|
||||
}
|
||||
0
|
||||
}
|
||||
return formats.sortedWith(fieldSorter)
|
||||
}
|
||||
|
|
@ -152,4 +172,101 @@ class FormatSorter(private var context: Context) {
|
|||
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
|
||||
}
|
||||
* */
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.Html
|
||||
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.util.Extensions.getIntByAny
|
||||
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.work.TerminalDownloadWorker
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import com.yausername.youtubedl_android.YoutubeDLRequest
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
|
@ -42,9 +46,11 @@ import java.util.regex.Pattern
|
|||
|
||||
class InfoUtil(private val context: Context) {
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
private lateinit var handler: Handler
|
||||
|
||||
init {
|
||||
try {
|
||||
handler = Handler(Looper.getMainLooper())
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
countryCODE = sharedPreferences.getString("locale", "")!!
|
||||
if (countryCODE.isEmpty()) countryCODE = "US"
|
||||
|
|
@ -131,6 +137,7 @@ class InfoUtil(private val context: Context) {
|
|||
if (nextpage == "null") nextpage = ""
|
||||
return PlaylistTuple(nextpage, items)
|
||||
}catch (e: Exception){
|
||||
Log.e("PLST", e.toString())
|
||||
return PlaylistTuple(
|
||||
"",
|
||||
getFromYTDL("https://www.youtube.com/playlist?list=$id")
|
||||
|
|
@ -186,7 +193,7 @@ class InfoUtil(private val context: Context) {
|
|||
}
|
||||
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
|
||||
try {
|
||||
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 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")){
|
||||
val formatsInJSON = obj.getJSONArray("audioStreams")
|
||||
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> {
|
||||
|
||||
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")
|
||||
if (m.find() && formatSource == "piped"){
|
||||
if (url.isYoutubeURL() && formatSource == "piped"){
|
||||
try {
|
||||
val id = getIDFromYoutubeURL(url)
|
||||
val res = genericRequest("$pipedURL/streams/$id")
|
||||
|
|
@ -455,11 +459,12 @@ class InfoUtil(private val context: Context) {
|
|||
getFormatsFromYTDL(url)
|
||||
}
|
||||
}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
|
||||
}
|
||||
|
||||
}catch(e: Exception) {
|
||||
println(e)
|
||||
if (e is CancellationException) throw e
|
||||
return getFormatsFromYTDL(url)
|
||||
}
|
||||
|
|
@ -502,32 +507,53 @@ class InfoUtil(private val context: Context) {
|
|||
|
||||
val res = YoutubeDL.getInstance().execute(request)
|
||||
val results: Array<String?> = try {
|
||||
val lineSeparator = System.getProperty("line.separator")
|
||||
res.out.split(lineSeparator!!).toTypedArray()
|
||||
res.out.split(System.lineSeparator()).toTypedArray()
|
||||
} catch (e: Exception) {
|
||||
arrayOf(res.out)
|
||||
}
|
||||
val json = results[0]
|
||||
val jsonArray = JSONArray(json)
|
||||
val jsonArray = kotlin.runCatching { JSONArray(json) }.getOrElse { JSONArray() }
|
||||
|
||||
return parseYTDLFormats(jsonArray)
|
||||
}
|
||||
|
||||
fun getFormatsMultiple(urls: List<String>, source: String? = null, progress: (progress: List<Format>) -> Unit){
|
||||
val urlsFile = File(context.cacheDir, "urls.txt")
|
||||
urlsFile.delete()
|
||||
urlsFile.createNewFile()
|
||||
urls.forEach {
|
||||
urlsFile.appendText(it+"\n")
|
||||
}
|
||||
data class MultipleFormatProgress(
|
||||
val url: String,
|
||||
val formats: List<Format>,
|
||||
val unavailable : Boolean = false,
|
||||
val unavailableMessage: String = ""
|
||||
)
|
||||
|
||||
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 p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
|
||||
val allYoutubeLinks = urls.any {p.matcher(it).find() }
|
||||
if (formatSource == "yt-dlp" || !allYoutubeLinks){
|
||||
val allYoutubeLinks = urls.all { it.isYoutubeURL() }
|
||||
if (allYoutubeLinks && formatSource == "piped") {
|
||||
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 {
|
||||
val request = YoutubeDLRequest(emptyList())
|
||||
request.addOption("--print", "%(formats)s")
|
||||
request.addOption("--print", "formats")
|
||||
request.addOption("-a", urlsFile.absolutePath)
|
||||
request.addOption("--skip-download")
|
||||
request.addOption("-R", "1")
|
||||
|
|
@ -537,7 +563,6 @@ class InfoUtil(private val context: Context) {
|
|||
request.addOption("-4")
|
||||
}
|
||||
|
||||
|
||||
if (sharedPreferences.getBoolean("use_cookies", false)){
|
||||
FileUtil.getCookieFile(context){
|
||||
request.addOption("--cookies", it)
|
||||
|
|
@ -557,35 +582,59 @@ class InfoUtil(private val context: Context) {
|
|||
}
|
||||
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
|
||||
|
||||
val txt = parseYTDLRequestString(request)
|
||||
println(txt)
|
||||
|
||||
var urlIdx = 0
|
||||
YoutubeDL.getInstance().execute(request){ progress, _, line ->
|
||||
try{
|
||||
if (line.isNotBlank()){
|
||||
val listOfStrings = JSONArray(line)
|
||||
progress(parseYTDLFormats(listOfStrings))
|
||||
val url = urls[urlIdx]
|
||||
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){
|
||||
progress(emptyList())
|
||||
Log.e("GET MULTIPLE FORMATS", e.toString())
|
||||
}
|
||||
urlIdx++
|
||||
}
|
||||
} 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()
|
||||
}
|
||||
e.printStackTrace()
|
||||
}
|
||||
}else{
|
||||
urls.forEach {
|
||||
val id = getIDFromYoutubeURL(it)
|
||||
val res = genericRequest("$pipedURL/streams/$id")
|
||||
createVideoFromPipedJSON(res, it)?.apply {
|
||||
progress(this.formats)
|
||||
}
|
||||
}
|
||||
|
||||
urlsFile.delete()
|
||||
}
|
||||
|
||||
urlsFile.delete()
|
||||
return formatCollection
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
|
|
@ -656,7 +705,7 @@ class InfoUtil(private val context: Context) {
|
|||
for (result in results) {
|
||||
if (result.isNullOrBlank()) continue
|
||||
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
|
||||
|
||||
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)
|
||||
Log.e("NOTE", format.toString())
|
||||
if (formatProper.format_note == null) continue
|
||||
|
||||
if (format.has("format_note")){
|
||||
|
|
@ -956,16 +1004,13 @@ class InfoUtil(private val context: Context) {
|
|||
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest {
|
||||
lateinit var request : YoutubeDLRequest
|
||||
|
||||
val matchFilter = mutableListOf<String>()
|
||||
request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){
|
||||
val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){
|
||||
YoutubeDLRequest(downloadItem.url)
|
||||
}else{
|
||||
YoutubeDLRequest(downloadItem.playlistURL!!).apply {
|
||||
if(downloadItem.playlistIndex == null){
|
||||
val matchPortion = downloadItem.url.split("/").last().split("=").last().split("&").first()
|
||||
matchFilter.add("id~='${matchPortion}'")
|
||||
addOption("--match-filter", "id~='${matchPortion}'")
|
||||
}else{
|
||||
addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}")
|
||||
}
|
||||
|
|
@ -1131,6 +1176,7 @@ class InfoUtil(private val context: Context) {
|
|||
when(type){
|
||||
DownloadViewModel.Type.audio -> {
|
||||
val supportedContainers = context.resources.getStringArray(R.array.audio_containers)
|
||||
var abrSort = ""
|
||||
|
||||
var audioQualityId : String = downloadItem.format.format_id
|
||||
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)){
|
||||
audioQualityId = "wa/w"
|
||||
}else if(audioQualityId.contains("kbps_ytdlnisgeneric")){
|
||||
matchFilter.add("abr<=${audioQualityId.split("kbps")[0]}")
|
||||
|
||||
abrSort = audioQualityId.split("kbps")[0]
|
||||
audioQualityId = ""
|
||||
}else{
|
||||
audioQualityId += "/ba/b"
|
||||
|
|
@ -1170,6 +1217,10 @@ class InfoUtil(private val context: Context) {
|
|||
formatSorting.append(",acodec:$aCodecPref")
|
||||
}
|
||||
|
||||
if (abrSort.isNotBlank()){
|
||||
formatSorting.append(",abr~${abrSort}")
|
||||
}
|
||||
|
||||
if(ext.isNotBlank()){
|
||||
if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){
|
||||
request.addOption("--audio-format", ext)
|
||||
|
|
@ -1284,8 +1335,9 @@ class InfoUtil(private val context: Context) {
|
|||
val preferredAudioLanguage = sharedPreferences.getString("audio_language", "")!!
|
||||
if (downloadItem.videoPreferences.removeAudio) audioF = ""
|
||||
|
||||
var abrSort = ""
|
||||
if(audioF.contains("kbps_ytdlnisgeneric")){
|
||||
matchFilter.add("abr<=${audioF.split("kbps")[0]}")
|
||||
abrSort = audioF.split("kbps")[0]
|
||||
audioF = ""
|
||||
}
|
||||
|
||||
|
|
@ -1403,6 +1455,7 @@ class InfoUtil(private val context: Context) {
|
|||
if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref")
|
||||
if (cont.isNotBlank()) append(",vext:$cont")
|
||||
if (acont.isNotBlank()) append(",aext:$acont")
|
||||
if (abrSort.isNotBlank()) append(",abr~${abrSort}")
|
||||
if (this.isNotBlank()){
|
||||
request.addOption("-S", "+hasaud$this")
|
||||
}
|
||||
|
|
@ -1466,10 +1519,6 @@ class InfoUtil(private val context: Context) {
|
|||
else -> {}
|
||||
}
|
||||
|
||||
if (matchFilter.isNotEmpty()) {
|
||||
request.addOption("--match-filter", matchFilter.joinToString(" & "))
|
||||
}
|
||||
|
||||
if (downloadItem.extraCommands.isNotBlank() && downloadItem.type != DownloadViewModel.Type.command){
|
||||
val cache = File(FileUtil.getCachePath(context))
|
||||
cache.mkdirs()
|
||||
|
|
@ -1546,7 +1595,8 @@ class InfoUtil(private val context: Context) {
|
|||
|
||||
fun getGenericVideoFormats(resources: Resources) : MutableList<Format>{
|
||||
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 containerPreference = sharedPreferences.getString("video_format", "")
|
||||
val audioCodecPreference = sharedPreferences.getString("audio_codec", "")!!.run {
|
||||
|
|
@ -1565,7 +1615,7 @@ class InfoUtil(private val context: Context) {
|
|||
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)) }
|
||||
return formats
|
||||
}
|
||||
|
|
@ -1574,7 +1624,7 @@ class InfoUtil(private val context: Context) {
|
|||
|
||||
class PlaylistTuple internal constructor(
|
||||
var nextPageToken: String,
|
||||
var videos: ArrayList<ResultItem>
|
||||
var videos: ArrayList<ResultItem>,
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -9,12 +9,9 @@ import android.view.MenuItem
|
|||
import android.widget.PopupMenu
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.get
|
||||
import androidx.core.view.isGone
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.deniscerri.ytdl.R
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import java.util.*
|
||||
|
||||
|
||||
object NavbarUtil {
|
||||
|
|
@ -28,7 +25,7 @@ object NavbarUtil {
|
|||
private var navItems = mapOf(
|
||||
"Home" to R.id.homeFragment,
|
||||
"History" to R.id.historyFragment,
|
||||
"Downloads" to R.id.downloadQueueMainFragment,
|
||||
"Queue" to R.id.downloadQueueMainFragment,
|
||||
"Terminal" to R.id.terminalActivity,
|
||||
"More" to R.id.moreFragment
|
||||
)
|
||||
|
|
@ -36,10 +33,11 @@ object NavbarUtil {
|
|||
fun getStartFragmentId(context: Context) : Int {
|
||||
val pref = settings.getString("start_destination", "")!!
|
||||
val items = getDefaultNavBarItems(context)
|
||||
val navBarHasPref = getNavBarPrefs().contains(items.indexOfFirst { it.itemId == navItems[pref] }.toString())
|
||||
return if (pref == "") {
|
||||
R.id.homeFragment
|
||||
}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> {
|
||||
return settings
|
||||
.getString("navigation_bar", "Home,History,More")!!
|
||||
.getString("navigation_bar", "0,1,2,-3,-4")!!
|
||||
.split(",")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,21 @@ class AlarmScheduler(private val context: Context) {
|
|||
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
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")
|
||||
fun schedule() {
|
||||
cancel()
|
||||
|
|
|
|||
5
app/src/main/res/drawable/baseline_access_alarm_24.xml
Normal file
5
app/src/main/res/drawable/baseline_access_alarm_24.xml
Normal 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>
|
||||
|
|
@ -58,6 +58,7 @@
|
|||
app:icon="@drawable/ic_music"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
android:contentDescription="@string/preferred_download_type"
|
||||
app:iconSize="11dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<androidx.core.widget.NestedScrollView android:layout_width="match_parent"
|
||||
android:scrollbars="none"
|
||||
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
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -127,4 +132,4 @@
|
|||
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
|
@ -1,19 +1,30 @@
|
|||
<?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_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/format_filter_linear"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<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_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/all"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
|
|
@ -73,4 +84,24 @@
|
|||
</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>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="all"
|
||||
android:text="@string/update_formats"
|
||||
android:text="@string/update"
|
||||
android:contentDescription="@string/update_formats"
|
||||
android:layout_marginStart="10dp"
|
||||
app:icon="@drawable/ic_refresh"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
|
@ -166,44 +167,6 @@
|
|||
android:orientation="vertical" />
|
||||
</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>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
|
|
|||
|
|
@ -4,109 +4,13 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
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_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/active_queued_linear"
|
||||
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>
|
||||
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="none"
|
||||
android:clipToPadding="false"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/pause"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/history_toolbar"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:title="@string/downloads"
|
||||
|
|
|
|||
|
|
@ -69,6 +69,22 @@
|
|||
android:paddingHorizontal="15dp"
|
||||
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
|
||||
android:id="@+id/download_queue"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -14,8 +14,11 @@
|
|||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/sort_by"
|
||||
android:textSize="12sp"
|
||||
android:layout_margin="20dp"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@
|
|||
android:padding="0dp"
|
||||
app:cornerRadius="10dp"
|
||||
app:icon="@drawable/ic_video"
|
||||
android:contentDescription="@string/preferred_download_type"
|
||||
app:iconTint="?attr/colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:contentDescription="@string/home"
|
||||
android:src="@drawable/ic_home_outlined"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
|
|
@ -39,6 +40,7 @@
|
|||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:singleLine="true"
|
||||
android:textDirection="locale"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="18sp"
|
||||
tools:text="Item" />
|
||||
|
|
|
|||
|
|
@ -86,13 +86,15 @@
|
|||
android:paddingHorizontal="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:hint="@string/url"
|
||||
app:endIconContentDescription="@string/import_from_clipboard"
|
||||
app:endIconMode="custom"
|
||||
app:endIconDrawable="@drawable/ic_clipboard"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:inputType="text"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="3"
|
||||
android:layout_height="wrap_content"
|
||||
/>
|
||||
|
||||
|
|
|
|||
17
app/src/main/res/layout/selectable_textview_filter.xml
Normal file
17
app/src/main/res/layout/selectable_textview_filter.xml
Normal 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" />
|
||||
|
|
@ -43,13 +43,13 @@
|
|||
|
||||
<string-array name="video_formats">
|
||||
<item>@string/best_quality</item>
|
||||
<item>2160p</item>
|
||||
<item>1440p</item>
|
||||
<item>1080p</item>
|
||||
<item>720p</item>
|
||||
<item>480p</item>
|
||||
<item>360p</item>
|
||||
<item>240p</item>
|
||||
<item>~2160p</item>
|
||||
<item>~1440p</item>
|
||||
<item>~1080p</item>
|
||||
<item>~720p</item>
|
||||
<item>~480p</item>
|
||||
<item>~360p</item>
|
||||
<item>~240p</item>
|
||||
<item>@string/worst_quality</item>
|
||||
</string-array>
|
||||
|
||||
|
|
@ -66,13 +66,13 @@
|
|||
</string-array>
|
||||
|
||||
<string-array name="audio_formats">
|
||||
<item>best</item>
|
||||
<item>192kbps</item>
|
||||
<item>160kbps</item>
|
||||
<item>128kbps</item>
|
||||
<item>96kbps</item>
|
||||
<item>64kbps</item>
|
||||
<item>worst</item>
|
||||
<item>@string/best_quality</item>
|
||||
<item>~192kbps</item>
|
||||
<item>~160kbps</item>
|
||||
<item>~128kbps</item>
|
||||
<item>~96kbps</item>
|
||||
<item>~64kbps</item>
|
||||
<item>@string/worst_quality</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="audio_formats_values">
|
||||
|
|
|
|||
|
|
@ -408,4 +408,6 @@
|
|||
<string name="queue">Queue</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="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>
|
||||
|
|
@ -161,6 +161,15 @@
|
|||
</PreferenceCategory>
|
||||
|
||||
<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
|
||||
android:widgetLayout="@layout/preferece_material_switch"
|
||||
app:defaultValue="false"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ buildscript {
|
|||
appCompatVer = '1.6.1'
|
||||
junitVer = '4.13.2'
|
||||
androidJunitVer = '1.1.5'
|
||||
espressoVer = '3.5.1'
|
||||
espressoVer = '3.6.1'
|
||||
jacksonVer = '2.9.8'
|
||||
// supports java 1.6
|
||||
commonsIoVer = '2.5'
|
||||
|
|
|
|||
Loading…
Reference in a new issue