1.7.6 beta

This commit is contained in:
zaednasr 2024-05-12 23:34:54 +02:00
parent 7f0cde7774
commit 0ce383f33e
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
78 changed files with 3072 additions and 1437 deletions

View file

@ -10,13 +10,10 @@ plugins {
def properties = new Properties()
def versionMajor = 1
def versionMinor = 7
def versionPatch = 5
def versionPatch = 6
def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def versionExt = ""
if (versionBuild > 0){
versionExt = ".${versionBuild}-beta"
}
def isBeta = true
def versionExt = isBeta ? ".${versionBuild}-beta" : ""
android {
namespace 'com.deniscerri.ytdl'

View file

@ -19,8 +19,15 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- for display over apps-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- alarm scheduler-->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<!-- queueing processing downloads in the background-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name=".App"
@ -127,6 +134,24 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="www.facebook.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="fb.watch" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="instagram.com" />
</intent-filter>
@ -136,6 +161,15 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="www.instagram.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="tiktok.com" />
</intent-filter>
@ -181,6 +215,33 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="www.reddit.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="reddit.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="m.reddit.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="pinterest.com" />
</intent-filter>
@ -356,6 +417,8 @@
<receiver android:name=".receiver.CancelDownloadNotificationReceiver" />
<receiver android:name=".receiver.CancelWorkReceiver" />
<receiver android:name=".receiver.PauseDownloadNotificationReceiver" />
<receiver android:name=".receiver.CancelScheduleAlarmReceiver" />
<receiver android:name=".receiver.ScheduleAlarmReceiver" />
<activity
android:name=".receiver.ResumeActivity"
@ -370,6 +433,8 @@
</intent-filter>
</activity>
<service android:name=".services.ProcessDownloadsInBackgroundService" android:exported="false" />
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"

View file

@ -12,6 +12,7 @@ import androidx.room.Upsert
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.DownloadItemSimple
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.repository.DownloadRepository
import kotlinx.coroutines.flow.Flow
@Dao
@ -130,7 +131,7 @@ interface DownloadDao {
fun getLastDownloadId(): Long
@Query("SELECT status FROM downloads WHERE id=:id")
fun checkStatus(id: Long) : String
fun checkStatus(id: Long) : DownloadRepository.Status?
@Query("UPDATE downloads " +
"SET status = CASE " +
@ -178,7 +179,7 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE id in (:list)")
suspend fun deleteAllWithIDs(list: List<Long>)
@Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','QueuedPaused','Active','ActivePaused')")
@Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','QueuedPaused','Active','ActivePaused', 'Scheduled')")
suspend fun cancelActiveQueued()
@Query("DELETE FROM downloads WHERE status='Processing' AND id=:id")
@ -229,9 +230,12 @@ interface DownloadDao {
@Query("Select url from downloads where id in (:ids)")
fun getURLsByID(ids: List<Long>) : List<String>
@Query("UPDATE downloads SET downloadStartTime=0 where id in (:list)")
@Query("UPDATE downloads SET downloadStartTime=0, status='Queued' where id in (:list)")
suspend fun resetScheduleTimeForItems(list: List<Long>)
@Query("UPDATE downloads SET status='Queued' where status in ('QueuedPaused', 'ActivePaused')")
suspend fun resetPausedItems()
@Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)")
suspend fun reQueueDownloadItems(list: List<Long>)

View file

@ -37,6 +37,9 @@ interface ResultDao {
return insertMultiple(items.filter { getResultByURL(it.url) == null })
}
@Query("SELECT * FROM results WHERE id IN (:ids)")
fun getAllByIDs(ids: List<Long>) : List<ResultItem>
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(item: ResultItem)

View file

@ -0,0 +1,12 @@
package com.deniscerri.ytdl.database.models
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
data class AlreadyExistsItem(
var downloadItem: DownloadItem,
var historyID: Long? = null
) : Parcelable

View file

@ -222,6 +222,9 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")

View file

@ -129,6 +129,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return resultDao.getResultByURL(url)
}
fun getAllByIDs(ids: List<Long>) : List<ResultItem> {
return resultDao.getAllByIDs(ids)
}
suspend fun getResultsFromSource(inputQuery: String, resetResults: Boolean, addToResults: Boolean = true, singleItem: Boolean = false) : ArrayList<ResultItem> {
return when(getQueryType(inputQuery)){
SourceType.YOUTUBE_VIDEO -> {
@ -151,9 +155,9 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
}
fun getQueryType(inputQuery: String) : SourceType {
private fun getQueryType(inputQuery: String) : SourceType {
var type = SourceType.SEARCH_QUERY
val p = Pattern.compile("(^(https?)://(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val p = Pattern.compile("((^(https?)://)?(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val m = p.matcher(inputQuery)
if (m.find()) {
type = SourceType.YOUTUBE_VIDEO

View file

@ -5,8 +5,11 @@ 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.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
@ -41,14 +44,18 @@ import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker
import com.google.gson.Gson
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import java.io.File
import java.util.Locale
@ -56,6 +63,7 @@ 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
@ -78,6 +86,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val scheduledDownloadsCount : Flow<Int>
val pausedDownloadsCount: Flow<Int>
val alreadyExistsUiState: MutableStateFlow<List<SharedDownloadViewModel.AlreadyExistsIDs>>
private var bestVideoFormat : Format
private var bestAudioFormat : Format
private var defaultVideoFormats : MutableList<Format>
@ -97,16 +107,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
private val historyRepository: HistoryRepository
private val resultRepository: ResultRepository
data class AlreadyExistsUIState(
var historyItems: MutableList<Long>,
var downloadItems : MutableList<Long>
)
val alreadyExistsUiState: MutableStateFlow<AlreadyExistsUIState> = MutableStateFlow(AlreadyExistsUIState(
historyItems = mutableListOf(),
downloadItems = mutableListOf()
))
enum class Type {
auto, audio, video, command
}
@ -121,6 +121,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
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)
@ -168,7 +170,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
audioContainer = sharedPreferences.getString("audio_format", "mp3")
bestAudioFormat = if (audioFormatIDPreference.isEmpty()){
infoUtil.getGenericAudioFormats(resources).last()
infoUtil.getGenericAudioFormats(resources).first()
}else{
Format(
audioFormatIDPreference.first().split("+").first(),
@ -204,132 +206,24 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return repository.getItemByID(id)
}
fun getAllByIDs(ids: List<Long>) : List<DownloadItem> {
return repository.getAllItemsByIDs(ids)
}
fun getHistoryItemById(id: Long) : HistoryItem? {
return historyRepository.getItem(id)
}
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
}
return sharedDownloadViewModel.getDownloadType(t, url)
}
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)s - %(title)s")
Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s")
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,
"",
resultItem.playlistTitle,
audioPreferences,
videoPreferences,
extraCommands,
customFileNameTemplate!!,
saveThumb,
DownloadRepository.Status.Queued.toString(), 0, null, playlistURL = resultItem.playlistURL, playlistIndex = resultItem.playlistIndex
)
return sharedDownloadViewModel.createDownloadItemFromResult(result, url, givenType)
}
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()
)
return sharedDownloadViewModel.createResultItemFromDownload(downloadItem)
}
fun createResultItemFromHistory(downloadItem: HistoryItem) : ResultItem {
@ -353,23 +247,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun createEmptyResultItem(url: String) : ResultItem {
return ResultItem(
0,
url,
"",
"",
"",
"",
"",
"",
arrayListOf(),
"",
arrayListOf(),
"",
null,
System.currentTimeMillis()
)
return sharedDownloadViewModel.createEmptyResultItem(url)
}
fun switchDownloadType(list: List<DownloadItem>, type: Type) : List<DownloadItem>{
@ -471,166 +349,140 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) 10 else 0}
sharedPreferences.getString("audio_language", "")?.apply {
if (this.isNotBlank()){
requirements.add { it: Format -> if (it.lang?.contains(this) == true) 3 else 0 }
}
}
requirements.add {it: Format -> if ("^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)) 2 else 0}
requirements.add {it: Format -> if (it.container == audioContainer) 1 else 0 }
return requirements
return sharedDownloadViewModel.getPreferredVideoRequirements()
}
//requirement and importance
@SuppressLint("RestrictedApi")
fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
//format id
requirements.add { it: Format -> if (formatIDPreference.contains(it.format_id)) 20 else 0 }
//resolutions
application.getStringArray(R.array.video_formats_values)
.filter { it.contains("_") }
.map{ it.split("_")[0].dropLast(1)
}.toMutableList().apply {
when(videoQualityPreference) {
"worst" -> {
requirements.add { it: Format -> if (it.format_note.contains("worst", ignoreCase = true)) (15) else 0 }
}
"best" -> {
requirements.add { it: Format -> if (it.format_note.contains("best", ignoreCase = true)) (15) else 0 }
}
else -> {
val preferenceIndex = this.indexOfFirst { videoQualityPreference.contains(it) }
val preference = this[preferenceIndex]
for(i in 0..preferenceIndex){
removeAt(0)
}
add(0, preference)
forEachIndexed { index, res ->
requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (15 - index - 1) else 0 }
}
}
}
}
requirements.add { it: Format -> if ("^(${videoCodec})(.+)?$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)) 5 else 0 }
requirements.add { it: Format -> if (it.acodec == "none" || it.acodec == "") 1 else 0 }
requirements.add { it: Format ->
if (videoContainer == "mp4")
if (it.container.equals("mpeg_4", true)) 1 else 0
else
if (it.container.equals(videoContainer, true)) 1 else 0
}
return requirements
return sharedDownloadViewModel.getPreferredVideoRequirements()
}
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" }
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 }
}
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)
}
}
return sharedDownloadViewModel.getFormat(formats, type)
}
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
return sharedDownloadViewModel.getPreferredAudioFormats(formats)
}
fun generateCommandFormat(c: CommandTemplate) : Format {
return Format(
c.title,
c.id.toString(),
"",
"",
"",
0,
c.content.replace("\n", " ")
)
return sharedDownloadViewModel.generateCommandFormat(c)
}
data class ProcessingItemsJob(
var jobID: Long = 0,
var job: Job? = null,
var itemType: String,
var itemIDs: List<Long>
)
fun getLatestCommandTemplateAsFormat() : Format {
val t = commandTemplateDao.getFirst()!!
return Format(t.title, "", "", "", "", 0, t.content)
}
fun turnResultItemsToDownloadItems(items: List<ResultItem?>) : List<DownloadItem> {
val list : MutableList<DownloadItem> = mutableListOf()
items.forEach {
val preferredType = getDownloadType(url = it!!.url).toString()
list.add(createDownloadItemFromResult(result = it, givenType = Type.valueOf(
preferredType
)))
val loadingProcessingItems: MutableStateFlow<Boolean> = MutableStateFlow(false)
var loadingProcessingDownloadsJobs: MutableStateFlow<List<ProcessingItemsJob>> = MutableStateFlow(listOf())
private fun cancelAllLoadingProcessingDownloads() {
loadingProcessingDownloadsJobs.value.onEach {
it.job?.cancel(CancellationException())
}
return list
}
fun getLoadingProcessingDownloadJob(jobID: Long) : ProcessingItemsJob? {
return loadingProcessingDownloadsJobs.value.firstOrNull { it.jobID == jobID }
}
fun cancelLoadingProcessingDownloads(jobID: Long) {
loadingProcessingDownloadsJobs.value.firstOrNull {it.jobID == jobID }?.apply {
this.job?.cancel(CancellationException())
}
}
fun turnDownloadItemsToProcessingDownloads(itemIDs: List<Long>) : Long {
val jobID = System.currentTimeMillis()
val job = viewModelScope.launch(Dispatchers.IO) {
val insertedIDs = mutableListOf<Long>()
try {
loadingProcessingItems.emit(true)
itemIDs.chunked(100).forEach { ids ->
val items = repository.getAllItemsByIDs(ids)
items.apply {
if (!isActive) {
throw CancellationException()
}
this.forEach {
it.id = 0
it.status = DownloadRepository.Status.Processing.toString()
insertedIDs.add(repository.insert(it))
}
}
}
loadingProcessingItems.emit(false)
} catch (e: Exception) {
repository.deleteAllWithIDs(insertedIDs)
loadingProcessingItems.emit(false)
}
}
viewModelScope.launch(Dispatchers.IO) {
val currentJobs = loadingProcessingDownloadsJobs.value.toMutableList()
currentJobs.add(
ProcessingItemsJob(jobID, job, DownloadItem::class.java.toString(), itemIDs)
)
loadingProcessingDownloadsJobs.emit(currentJobs)
}
return jobID
}
fun turnResultItemsToProcessingDownloads(itemIds: List<Long>, downloadNow: Boolean = false) : Long {
val jobID = System.currentTimeMillis()
val job = viewModelScope.launch(Dispatchers.IO) {
val insertedIds = mutableListOf<Long>()
try {
loadingProcessingItems.emit(true)
itemIds.chunked(100).forEach { ids ->
val items = resultRepository.getAllByIDs(ids)
val downloadItems = items.map {
val preferredType = getDownloadType(url = it.url).toString()
val tmp = createDownloadItemFromResult(result = it, givenType = Type.valueOf(
preferredType
))
tmp.status = DownloadRepository.Status.Processing.toString()
tmp
}
if (!isActive) {
throw CancellationException()
}
if (downloadNow) {
downloadItems.forEach {
it.status = DownloadRepository.Status.Queued.toString()
}
queueDownloads(downloadItems)
}else{
downloadItems.forEach {
insertedIds.add(repository.insert(it))
}
}
}
loadingProcessingItems.emit(false)
}catch (e: Exception) {
repository.deleteAllWithIDs(insertedIds)
loadingProcessingItems.emit(false)
}
}
viewModelScope.launch(Dispatchers.IO) {
val currentJobs = loadingProcessingDownloadsJobs.value.toMutableList()
currentJobs.add(
ProcessingItemsJob(jobID, job, ResultItem::class.java.toString(), itemIds)
)
loadingProcessingDownloadsJobs.emit(currentJobs)
}
return jobID
}
fun insert(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){
@ -688,6 +540,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) {
cancelAllLoadingProcessingDownloads()
repository.cancelActiveQueued()
}
@ -706,11 +559,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dao.getSavedDownloadsList()
}
private fun cloneFormat(item: Format) : Format {
val string = Gson().toJson(item, Format::class.java)
return Gson().fromJson(string, Format::class.java)
}
fun getActiveDownloads() : List<DownloadItem>{
return repository.getActiveDownloads()
}
@ -725,6 +573,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.resetScheduleTimeForItems(items)
dbManager.downloadDao.resetPausedItems()
repository.startDownloadWorker(emptyList(), application)
}
@ -789,173 +638,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao.updateDownloadID(-current, id)
}
suspend fun reQueueDownloadItems(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
fun reQueueDownloadItems(items: List<Long>) = viewModelScope.launch(Dispatchers.IO) {
dbManager.downloadDao.reQueueDownloadItems(items)
repository.startDownloadWorker(emptyList(), application)
}
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : Pair<List<Long>, List<Long>> {
val context = App.instance
val alarmScheduler = AlarmScheduler(context)
val activeAndQueuedDownloads = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloads()
}
val queuedItems = mutableListOf<DownloadItem>()
val existingDownloads = mutableListOf<Long>()
val existingHistory = mutableListOf<Long>()
//if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
alarmScheduler.schedule()
}
if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){
items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" }
}
val downloadArchive = runCatching { File(FileUtil.getDownloadArchivePath(application)).useLines { it.toList() } }.getOrElse { listOf() }
.map { it.split(" ")[1] }
items.forEach {
if (! listOf(DownloadRepository.Status.ActivePaused, DownloadRepository.Status.Scheduled).toListString().contains(it.status))
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
}
existingDownloads.add(it.id)
}
}
"url_type" -> {
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
}
if (existingDownload != null){
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
alreadyExists = true
existingDownloads.add(id)
}else{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
val existingHistoryItem = history.firstOrNull {
h -> h.type == it.type
}
if (existingHistoryItem != null){
alreadyExists = true
existingHistory.add(existingHistoryItem.id)
}
}
}
"config" -> {
val currentCommand = infoUtil.buildYoutubeDLRequest(it)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
}
if (existingDownload != null){
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
alreadyExists = true
existingDownloads.add(id)
}else{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
val existingHistoryItem = history.firstOrNull {
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
}
if (existingHistoryItem != null){
alreadyExists = true
existingHistory.add(existingHistoryItem.id)
}
}
}
}
}
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 (existingDownloads.isNotEmpty() || existingHistory.isNotEmpty()){
alreadyExistsUiState.update { u -> u.copy(existingHistory, existingDownloads) }
}
if (queuedItems.isNotEmpty()){
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime()){
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 Pair(existingDownloads, existingHistory)
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : List<SharedDownloadViewModel.AlreadyExistsIDs> {
val ids = sharedDownloadViewModel.queueDownloads(items, ign)
return ids
}
fun getQueuedCollectedFileSize() : Long {
@ -978,19 +668,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao.updateProcessingtoSavedStatus()
}
suspend fun downloadProcessingDownloads(timeInMillis: Long = 0){
repository.getProcessingDownloads().apply {
if (timeInMillis > 0){
this.forEach {
it.status = DownloadRepository.Status.Scheduled.toString()
it.downloadStartTime = timeInMillis
}
}
queueDownloads(this)
}
}
suspend fun updateProcessingFormat(selectedFormats: List<FormatTuple>): List<Long> {
val items = repository.getProcessingDownloads()
items.forEachIndexed { index, i ->
@ -1098,16 +775,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val first = dao.getFirstProcessingDownload()
}
suspend fun addDownloadsToProcessing(ids: List<Long>) {
repository.deleteProcessing()
val items = repository.getAllItemsByIDs(ids)
items.forEach {
it.id = 0
it.status = DownloadRepository.Status.Processing.toString()
insert(it)
}
}
fun getURLsByStatus(list: List<DownloadRepository.Status>) : List<String> {
return dao.getURLsByStatus(list.map { it.toString() })
}

View file

@ -10,6 +10,7 @@ import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
@ -124,7 +125,15 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
}
//schedule for next time
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
.addTag("observeSources")
.addTag(it.id.toString())

View file

@ -0,0 +1,616 @@
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.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 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)s - %(title)s")
Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s")
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,
"",
resultItem.playlistTitle,
audioPreferences,
videoPreferences,
extraCommands,
customFileNameTemplate!!,
saveThumb,
DownloadRepository.Status.Queued.toString(), 0, null, playlistURL = resultItem.playlistURL, playlistIndex = resultItem.playlistIndex
)
}
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()
)
}
private fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) 10 else 0}
sharedPreferences.getString("audio_language", "")?.apply {
if (this.isNotBlank()){
requirements.add { it: Format -> if (it.lang?.contains(this) == true) 3 else 0 }
}
}
requirements.add {it: Format -> if ("^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)) 2 else 0}
requirements.add {it: Format -> if (it.container == audioContainer) 1 else 0 }
return requirements
}
//requirement and importance
@SuppressLint("RestrictedApi")
fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
//format id
requirements.add { it: Format -> if (formatIDPreference.contains(it.format_id)) 20 else 0 }
//resolutions
context.getStringArray(R.array.video_formats_values)
.filter { it.contains("_") }
.map{ it.split("_")[0].dropLast(1)
}.toMutableList().apply {
when(videoQualityPreference) {
"worst" -> {
requirements.add { it: Format -> if (it.format_note.contains("worst", ignoreCase = true)) (15) else 0 }
}
"best" -> {
requirements.add { it: Format -> if (it.format_note.contains("best", ignoreCase = true)) (15) else 0 }
}
else -> {
val preferenceIndex = this.indexOfFirst { videoQualityPreference.contains(it) }
val preference = this[preferenceIndex]
for(i in 0..preferenceIndex){
removeAt(0)
}
add(0, preference)
forEachIndexed { index, res ->
requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (15 - index - 1) else 0 }
}
}
}
}
requirements.add { it: Format -> if ("^(${videoCodec})(.+)?$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)) 5 else 0 }
requirements.add { it: Format -> if (it.acodec == "none" || it.acodec == "") 1 else 0 }
requirements.add { it: Format ->
if (videoContainer == "mp4")
if (it.container.equals("mpeg_4", true)) 1 else 0
else
if (it.container.equals(videoContainer, true)) 1 else 0
}
return requirements
}
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" }
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 }
}
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 (! listOf(DownloadRepository.Status.ActivePaused, DownloadRepository.Status.Scheduled).toListString().contains(it.status))
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()){
repository.startDownloadWorker(queuedItems, context)
if(!useScheduler){
queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
CoroutineScope(Dispatchers.IO).launch {
runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}else{
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
CoroutineScope(Dispatchers.IO).launch {
runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}
}
}
return existingItemIDs
}
}

View file

@ -0,0 +1,34 @@
package com.deniscerri.ytdl.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.work.CancelScheduledDownloadWorker
import com.deniscerri.ytdl.work.DownloadWorker
import java.util.concurrent.TimeUnit
class CancelScheduleAlarmReceiver : BroadcastReceiver() {
override fun onReceive(ctx: Context?, p1: Intent?) {
ctx?.apply {
val workConstraints = Constraints.Builder()
val workRequest2 = OneTimeWorkRequestBuilder<CancelScheduledDownloadWorker>()
.addTag("cancelScheduledDownload")
.setConstraints(workConstraints.build())
.setInitialDelay( 0L, TimeUnit.MILLISECONDS)
WorkManager.getInstance(this).enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest2.build()
)
}
}
}

View file

@ -0,0 +1,38 @@
package com.deniscerri.ytdl.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.work.DownloadWorker
import java.util.concurrent.TimeUnit
class ScheduleAlarmReceiver : BroadcastReceiver() {
override fun onReceive(ctx: Context?, p1: Intent?) {
ctx?.apply {
val workConstraints = Constraints.Builder()
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val allowMeteredNetworks = preferences.getBoolean("metered_networks", true)
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("scheduledDownload")
.addTag("download")
.setConstraints(workConstraints.build())
.setInitialDelay(0L, TimeUnit.MILLISECONDS)
WorkManager.getInstance(this).enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
}
}

View file

@ -21,6 +21,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import androidx.core.app.ActivityCompat
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.lifecycle.ViewModelProvider
@ -34,9 +35,11 @@ import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.CookieViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -52,6 +55,7 @@ class ShareActivity : BaseActivity() {
lateinit var context: Context
private lateinit var resultViewModel: ResultViewModel
private lateinit var historyViewModel: HistoryViewModel
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var cookieViewModel: CookieViewModel
private lateinit var sharedPreferences: SharedPreferences
@ -115,6 +119,7 @@ class ShareActivity : BaseActivity() {
context = baseContext
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
@ -208,6 +213,18 @@ class ShareActivity : BaseActivity() {
}
this@ShareActivity.finish()
}
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.isNotEmpty()){
withContext(Dispatchers.Main){
val bundle = bundleOf(
Pair("duplicates", res)
)
navController.navigate(R.id.downloadsAlreadyExistDialog2, bundle)
}
downloadViewModel.alreadyExistsUiState.value = mutableListOf()
}
}
}
}
}

View file

@ -0,0 +1,136 @@
package com.deniscerri.ytdl.services
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import androidx.core.content.IntentCompat
import com.deniscerri.ytdl.database.DBManager
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.repository.ResultRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.SharedDownloadViewModel
import com.deniscerri.ytdl.util.NotificationUtil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class ProcessDownloadsInBackgroundService : Service() {
private val binder: IBinder = LocalBinder()
private val queueProcessingDownloadsJobList = mutableListOf<Job>()
private lateinit var repository: DownloadRepository
private lateinit var resultRepository: ResultRepository
private lateinit var downloadViewModel: SharedDownloadViewModel
inner class LocalBinder: Binder() {
val service: ProcessDownloadsInBackgroundService
get() = this@ProcessDownloadsInBackgroundService
}
override fun onCreate() {
super.onCreate()
val dbManager = DBManager.getInstance(this)
repository = DownloadRepository(dbManager.downloadDao)
resultRepository = ResultRepository(dbManager.resultDao, this)
downloadViewModel = SharedDownloadViewModel(this)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val notificationUtil = NotificationUtil(this)
startForeground(System.currentTimeMillis().toInt(), notificationUtil.createProcessingDownloads())
val itemType = intent.getStringExtra("itemType") ?: ""
val itemIDs = intent.getLongArrayExtra("itemIDs") ?: longArrayOf()
val jobData = DownloadViewModel.ProcessingItemsJob(
itemType = itemType,
itemIDs = itemIDs.toList()
)
val timeInMillis = intent.getLongExtra("timeInMillis", 0)
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
runJob(jobData, timeInMillis)
}
return super.onStartCommand(intent, flags, startId)
}
override fun onBind(intent: Intent): IBinder {
return binder
}
private fun DownloadItem.setAsScheduling(timeInMillis: Long) {
status = DownloadRepository.Status.Scheduled.toString()
downloadStartTime = timeInMillis
}
private suspend fun runJob(jobData: DownloadViewModel.ProcessingItemsJob, timeInMillis: Long = 0) {
val job = CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
if (jobData.itemType != "") {
val itemIDS = jobData.itemIDs
val processingType = jobData.itemType
when(processingType) {
ResultItem::class.java.toString() -> {
itemIDS.chunked(100).map { ids ->
resultRepository.getAllByIDs(ids).map {
downloadViewModel.createDownloadItemFromResult(
result = it, givenType = DownloadViewModel.Type.valueOf(
downloadViewModel.getDownloadType(url = it.url).toString()
)
)
}.apply {
if (timeInMillis > 0) {
this.forEach {
it.setAsScheduling(timeInMillis)
}
}
downloadViewModel.queueDownloads(this)
}
}
}
DownloadItem::class.java.toString() -> {
itemIDS.chunked(100).map { ids ->
repository.getAllItemsByIDs(ids).apply {
if (timeInMillis > 0) {
this.forEach {
it.setAsScheduling(timeInMillis)
}
}
downloadViewModel.queueDownloads(this)
}
}
}
}
}else {
repository.getProcessingDownloads().apply {
if (timeInMillis > 0){
this.forEach {
it.setAsScheduling(timeInMillis)
}
}
downloadViewModel.queueDownloads(this)
}
}
}
job.invokeOnCompletion {
queueProcessingDownloadsJobList.remove(job)
if (queueProcessingDownloadsJobList.isEmpty()){
stopForeground(true)
stopSelf()
}
}
queueProcessingDownloadsJobList.add(job)
}
fun cancelAllProcessingJobs(){
queueProcessingDownloadsJobList.onEach { it.cancel(CancellationException()) }
}
}

View file

@ -20,6 +20,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.view.forEach
import androidx.core.view.isVisible
@ -59,12 +60,14 @@ import com.google.android.material.search.SearchBar
import com.google.android.material.search.SearchView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.collections.ArrayList
class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggestionsAdapter.OnItemClickListener, OnClickListener {
@ -76,7 +79,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
private var downloadSelectedFab: ExtendedFloatingActionButton? = null
private var downloadAllFab: ExtendedFloatingActionButton? = null
private var clipboardFab: ExtendedFloatingActionButton? = null
private var homeFabs: CoordinatorLayout? = null
private var homeFabs: LinearLayout? = null
private var infoUtil: InfoUtil? = null
private var downloadQueue: ArrayList<ResultItem>? = null
@ -216,12 +219,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
inputQueries!!.addAll(argList)
}
if(arguments?.getBoolean("search") == true){
requireView().post {
searchBar?.performClick()
}
}
if (inputQueries != null) {
lifecycleScope.launch(Dispatchers.IO){
resultViewModel.deleteAll()
@ -276,22 +273,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
lifecycleScope.launch {
launch{
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.downloadItems.isNotEmpty() || res.historyItems.isNotEmpty()) {
if (res.isNotEmpty()){
withContext(Dispatchers.Main){
kotlin.runCatching {
UiUtil.handleExistingDownloadsResponse(
requireActivity(),
requireActivity().lifecycleScope,
requireActivity().supportFragmentManager,
res,
downloadViewModel,
historyViewModel)
}
val bundle = bundleOf(
Pair("duplicates", ArrayList(res))
)
delay(500)
findNavController().navigate(R.id.downloadsAlreadyExistDialog, bundle)
}
downloadViewModel.alreadyExistsUiState.value = DownloadViewModel.AlreadyExistsUIState(
mutableListOf(),
mutableListOf()
)
downloadViewModel.alreadyExistsUiState.value = mutableListOf()
}
}
}
@ -313,15 +303,24 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
arguments?.remove("showDownloadsWithUpdatedFormats")
CoroutineScope(Dispatchers.IO).launch {
val ids = arguments?.getLongArray("downloadIds") ?: return@launch
downloadViewModel.updateItemsWithIdsToProcessingStatus(ids.toList())
val jobID = downloadViewModel.turnDownloadItemsToProcessingDownloads(ids.toList())
withContext(Dispatchers.Main){
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
Pair("processingDownloadsJobID", jobID)
))
}
}
}
if(arguments?.getBoolean("search") == true){
arguments?.remove("search")
requireView().post {
searchBar?.performClick()
}
}
if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){
updateSearchViewItems(searchView?.editText?.text)
updateSearchViewItems(searchView?.editText?.text.toString())
}
requireView().post {
@ -405,13 +404,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
chipGroupDivider?.visibility = VISIBLE
}
updateSearchViewItems(searchView!!.editText.text)
updateSearchViewItems(searchView!!.editText.text.toString())
}
}
searchView!!.editText.doAfterTextChanged {
if (searchView!!.currentTransitionState != SearchView.TransitionState.SHOWN) return@doAfterTextChanged
updateSearchViewItems(it)
updateSearchViewItems(it.toString())
}
searchView!!.editText.setOnTouchListener(OnTouchListener { _, event ->
@ -454,7 +453,11 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
searchBar!!.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) {
R.id.delete_results -> {
resultViewModel.cancelParsingQueries()
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultViewModel.cancelParsingQueries()
}
}
resultViewModel.getTrending()
selectedObjects = ArrayList()
searchBar!!.setText("")
@ -479,7 +482,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
@SuppressLint("InflateParams")
private fun updateSearchViewItems(searchQuery: Editable?) = lifecycleScope.launch(Dispatchers.Main) {
private fun updateSearchViewItems(searchQuery: String) = lifecycleScope.launch(Dispatchers.Main) {
lifecycleScope.launch {
if (searchView!!.editText.text.isEmpty()){
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
@ -490,13 +493,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
val combinedList = mutableListOf<SearchSuggestionItem>()
val history = withContext(Dispatchers.IO){
resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery!!) }
resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery) }
}.map {
SearchSuggestionItem(it, SearchSuggestionType.HISTORY)
}
val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){
withContext(Dispatchers.IO){
infoUtil!!.getSearchSuggestions(searchQuery.toString())
infoUtil!!.getSearchSuggestions(searchQuery)
}
}else{
emptyList()
@ -509,12 +512,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
val url = checkClipboard()
url?.apply {
if (this.isNotEmpty()){
var alreadyHasThem = this.all { queriesChipGroup?.children?.any { c -> (c as Chip).text.contains(it) } == true }
if (this.isNotEmpty() && !alreadyHasThem){
combinedList.add(0, SearchSuggestionItem(this.joinToString("\n"), SearchSuggestionType.CLIPBOARD))
}
}
searchSuggestionsAdapter?.submitList(combinedList)
// history.forEach { s ->
@ -729,21 +732,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} catch (e: Exception) {""}
if (viewIdName.isNotEmpty()) {
if (viewIdName == "downloadAll") {
lifecycleScope.launch {
val downloadList = withContext(Dispatchers.IO){
downloadViewModel.turnResultItemsToDownloadItems(resultsList!!)
}
if (sharedPreferences!!.getBoolean("download_card", true)) {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.insertToProcessing(downloadList)
}
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
} else {
downloadList.chunked(100).forEach {
downloadViewModel.queueDownloads(it)
}
}
val showDownloadCard = sharedPreferences!!.getBoolean("download_card", true)
val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(resultsList!!.map { it!!.id }, downloadNow = !showDownloadCard)
if (showDownloadCard){
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
Pair("processingDownloadsJobID", jobID)
))
}
}
}
@ -806,24 +800,18 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
R.id.download -> {
lifecycleScope.launch {
if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects.size == 1) {
val showDownloadCard = sharedPreferences!!.getBoolean("download_card", true)
if (showDownloadCard && selectedObjects.size == 1) {
showSingleDownloadSheet(
selectedObjects[0],
downloadViewModel.getDownloadType(url = selectedObjects[0].url)
)
}else{
val downloadList = withContext(Dispatchers.IO){
downloadViewModel.turnResultItemsToDownloadItems(selectedObjects)
}
if (sharedPreferences!!.getBoolean("download_card", true)) {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.insertToProcessing(downloadList)
}
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
} else {
downloadViewModel.queueDownloads(downloadList)
val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(selectedObjects.map { it.id }, downloadNow = !showDownloadCard)
if (showDownloadCard){
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
Pair("processingDownloadsJobID", jobID)
))
}
}
clearCheckedItems()
@ -835,7 +823,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
homeAdapter?.checkAll(resultsList)
selectedObjects.clear()
resultsList?.forEach { selectedObjects.add(it!!) }
mode?.title = getString(R.string.all_items_selected)
mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {
@ -905,14 +893,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
override fun onSearchSuggestionAdd(t: String) {
val items = t.split("\n")
override fun onSearchSuggestionAdd(text: String) {
val items = text.split("\n")
items.forEach {text ->
val present = queriesChipGroup!!.children.firstOrNull { (it as Chip).text.toString() == text }
items.forEach {t ->
val present = queriesChipGroup!!.children.firstOrNull { (it as Chip).text.toString() == t }
if (present == null) {
val chip = layoutinflater!!.inflate(R.layout.input_chip, queriesChipGroup, false) as Chip
chip.text = text
chip.text = t
chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK))
chip.setOnClickListener {
if (queriesChipGroup!!.childCount == 1) queriesConstraint!!.visibility = View.GONE
@ -924,17 +912,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
searchView!!.editText.setText("")
if (queriesChipGroup!!.childCount == 0) queriesConstraint!!.visibility = View.GONE
else queriesConstraint!!.visibility = View.VISIBLE
queriesConstraint?.isVisible = queriesChipGroup?.childCount!! > 0
val clipBoardItem = searchSuggestionsRecyclerView?.layoutManager?.findViewByPosition(0)
clipBoardItem?.apply {
if ((this as ConstraintLayout).findViewById<TextView>(R.id.suggestion_text).text == getString(R.string.link_you_copied)){
searchSuggestionsAdapter?.notifyItemRemoved(0)
searchSuggestionsAdapter?.getList()?.apply {
if (this.first().type == SearchSuggestionType.CLIPBOARD){
val newList = this.toMutableList().drop(1)
searchSuggestionsAdapter?.submitList(newList)
}
}
}
override fun onSearchSuggestionLongClick(text: String, position: Int) {
@ -943,7 +928,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
resultViewModel.removeSearchQueryFromHistory(text)
updateSearchViewItems(searchView!!.editText.text)
updateSearchViewItems(searchView!!.editText.text.toString())
}
deleteDialog.show()
}

View file

@ -2,27 +2,34 @@ package com.deniscerri.ytdl.ui.adapter
import android.app.Activity
import android.content.SharedPreferences
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.PopupMenu
import android.widget.TextView
import androidx.core.view.isVisible
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.AlreadyExistsItem
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.Extensions.loadThumbnail
import com.deniscerri.ytdl.util.Extensions.popup
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.card.MaterialCardView
class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<Pair<DownloadItem, Long?>, AlreadyExistsAdapter.ViewHolder>(AsyncDifferConfig.Builder(
class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<AlreadyExistsItem, AlreadyExistsAdapter.ViewHolder>(AsyncDifferConfig.Builder(
DIFF_CALLBACK
).build()) {
private val onItemClickListener: OnItemClickListener
@ -39,7 +46,7 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
val cardView: MaterialCardView
init {
cardView = itemView as MaterialCardView
cardView = itemView.findViewById(R.id.download_card_view)
}
}
@ -51,12 +58,9 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val alreadyExistsItem = getItem(position) ?: return
val item = alreadyExistsItem.first
val historyID = alreadyExistsItem.second
val card = holder.cardView
card.tag = item.id.toString()
card.popup()
card.tag = alreadyExistsItem.downloadItem.id.toString()
val item = alreadyExistsItem.downloadItem
val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
@ -76,6 +80,20 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
}
itemTitle.text = title.ifEmpty { item.url }
//DOWNLOAD TYPE -----------------------------
val type = card.findViewById<TextView>(R.id.download_type)
when(item.type){
DownloadViewModel.Type.audio -> type.setCompoundDrawablesRelativeWithIntrinsicBounds(
R.drawable.ic_music_formatcard, 0,0,0
)
DownloadViewModel.Type.video -> type.setCompoundDrawablesRelativeWithIntrinsicBounds(
R.drawable.ic_video_formatcard, 0,0,0
)
else -> type.setCompoundDrawablesRelativeWithIntrinsicBounds(
R.drawable.ic_terminal_formatcard, 0,0,0
)
}
val formatNote = card.findViewById<TextView>(R.id.format_note)
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
View.GONE
@ -102,45 +120,64 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
if (fileSizeReadable == "?") fileSize.visibility = View.GONE
else fileSize.text = fileSizeReadable
val editBtn = card.findViewById<Button>(R.id.already_exists_edit)
val deleteBtn = card.findViewById<Button>(R.id.already_exists_delete)
val menu = card.findViewById<View>(R.id.options)
menu.isVisible = true
menu.setOnClickListener {
val popup = PopupMenu(activity, it)
popup.menuInflater.inflate(R.menu.already_exists_menu, popup.menu)
if (Build.VERSION.SDK_INT > 27) popup.menu.setGroupDividerEnabled(true)
popup.setOnMenuItemClickListener { m ->
when(m.itemId){
R.id.edit -> {
onItemClickListener.onEditItem(alreadyExistsItem, position)
popup.dismiss()
}
R.id.delete -> {
onItemClickListener.onDeleteItem(alreadyExistsItem, position)
popup.dismiss()
}
R.id.copy_url -> {
UiUtil.copyLinkToClipBoard(activity, item.url)
popup.dismiss()
}
}
true
}
popup.show()
}
card.setOnLongClickListener {
onItemClickListener.onDeleteItem(item, position, historyID)
onItemClickListener.onDeleteItem(alreadyExistsItem, position)
true
}
editBtn.setOnClickListener {
onItemClickListener.onEditItem(item, position)
}
deleteBtn.setOnClickListener {
onItemClickListener.onDeleteItem(item, position, historyID)
}
card.setOnClickListener(null)
if (historyID != null){
if (alreadyExistsItem.historyID != null){
card.setOnClickListener {
onItemClickListener.onShowHistoryItem(historyID)
onItemClickListener.onShowHistoryItem(alreadyExistsItem.historyID!!)
}
}else{
card.setOnClickListener(null)
}
}
interface OnItemClickListener {
fun onEditItem(downloadItem: DownloadItem, position: Int)
fun onDeleteItem(downloadItem: DownloadItem, position: Int, historyID: Long?)
fun onShowHistoryItem(id: Long)
fun onEditItem(alreadyExistsItem: AlreadyExistsItem, position: Int)
fun onDeleteItem(alreadyExistsItem: AlreadyExistsItem, position: Int)
fun onShowHistoryItem(historyItemID: Long)
}
companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<Pair<DownloadItem, Long?>> = object : DiffUtil.ItemCallback<Pair<DownloadItem, Long?>>() {
override fun areItemsTheSame(oldItem: Pair<DownloadItem, Long?>, newItem: Pair<DownloadItem, Long?>): Boolean {
return oldItem.first.id == newItem.first.id
private val DIFF_CALLBACK: DiffUtil.ItemCallback<AlreadyExistsItem> = object : DiffUtil.ItemCallback<AlreadyExistsItem>() {
override fun areItemsTheSame(oldItem: AlreadyExistsItem, newItem: AlreadyExistsItem): Boolean {
return oldItem.downloadItem.id == newItem.downloadItem.id
}
override fun areContentsTheSame(oldItem: Pair<DownloadItem, Long?>, newItem: Pair<DownloadItem, Long?>): Boolean {
return oldItem.first.id == newItem.first.id && oldItem.first.title == newItem.first.title && oldItem.first.author == newItem.first.author && oldItem.first.thumb == newItem.first.thumb
override fun areContentsTheSame(oldItem: AlreadyExistsItem, newItem: AlreadyExistsItem): Boolean {
return oldItem.downloadItem.id == newItem.downloadItem.id && oldItem.downloadItem.title == newItem.downloadItem.title && oldItem.downloadItem.author == newItem.downloadItem.author && oldItem.downloadItem.thumb == newItem.downloadItem.thumb
}
}
}

View file

@ -66,6 +66,7 @@ class ScheduledDownloadAdapter(onItemClickListener: OnItemClickListener, activit
if (item == null) return
card.tag = item.id.toString()
holder.itemView.tag = item.id.toString()
//Scheduled Time
val time = mainView.findViewById<TextView>(R.id.scheduled_time)
@ -152,7 +153,7 @@ class ScheduledDownloadAdapter(onItemClickListener: OnItemClickListener, activit
if (checkedItems.size > 0 || inverted) {
checkCard(card, item.id, position)
} else {
onItemClickListener.onCardClick(item.id)
onItemClickListener.onCardClick(item.id, position)
}
}
@ -218,7 +219,7 @@ class ScheduledDownloadAdapter(onItemClickListener: OnItemClickListener, activit
interface OnItemClickListener {
fun onActionButtonClick(itemID: Long)
fun onCardClick(itemID: Long)
fun onCardClick(itemID: Long, position: Int)
fun onCardSelect(isChecked: Boolean, position: Int)
}
@ -230,7 +231,11 @@ class ScheduledDownloadAdapter(onItemClickListener: OnItemClickListener, activit
}
override fun areContentsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean {
return oldItem.id == newItem.id && oldItem.title == newItem.title && oldItem.author == newItem.author && oldItem.thumb == newItem.thumb
return oldItem.id == newItem.id &&
oldItem.title == newItem.title &&
oldItem.author == newItem.author &&
oldItem.thumb == newItem.thumb &&
oldItem.downloadStartTime == newItem.downloadStartTime
}
}
}

View file

@ -52,6 +52,7 @@ class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activit
val linear = holder.linear
when (item.type){
SearchSuggestionType.SUGGESTION -> {
holder.itemView.tag = SearchSuggestionType.SUGGESTION.toString()
val textView = linear.findViewById<TextView>(R.id.suggestion_text)
textView.text = item.text
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_search, 0, 0, 0)
@ -67,6 +68,7 @@ class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activit
}
}
SearchSuggestionType.HISTORY -> {
holder.itemView.tag = SearchSuggestionType.HISTORY.toString()
val textView = linear.findViewById<TextView>(R.id.suggestion_text)
textView.text = item.text
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0)
@ -85,6 +87,7 @@ class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activit
}
}
SearchSuggestionType.CLIPBOARD -> {
holder.itemView.tag = SearchSuggestionType.CLIPBOARD.toString()
val textView = linear.findViewById<TextView>(R.id.suggestion_text)
textView.text = activity.getString(R.string.link_you_copied)
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_language, 0, 0, 0)
@ -102,6 +105,9 @@ class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activit
}
}
}
fun getList() : List<SearchSuggestionItem> {
return this.currentList
}
interface OnItemClickListener {
fun onSearchSuggestionClick(text: String)

View file

@ -0,0 +1,38 @@
package com.deniscerri.ytdl.ui.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.databinding.SortableTextItemBinding
class SortableTextItemAdapter(
val items: MutableList<Pair<String, String>>
) : RecyclerView.Adapter<SortableTextItemAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView
init {
textView = itemView.findViewById(R.id.textContent)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = SortableTextItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding.root)
}
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items.toList()[position]
holder.textView.text = item.second
holder.textView.tag = item.first
}
}

View file

@ -12,6 +12,8 @@ import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
@ -28,12 +30,17 @@ import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.ChapterItem
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.util.Extensions
import com.deniscerri.ytdl.util.Extensions.convertToTimestamp
import com.deniscerri.ytdl.util.Extensions.setTextAndRecalculateWidth
import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.util.Extensions.toStringTimeStamp
import com.deniscerri.ytdl.util.Extensions.toTimePeriodsArray
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.VideoPlayerUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
@ -55,6 +62,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.reflect.Type
import java.util.*
import kotlin.math.min
import kotlin.properties.Delegates
@ -68,10 +76,11 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
private lateinit var progress : ProgressBar
private lateinit var pauseBtn : MaterialButton
private lateinit var rewindBtn : MaterialButton
private lateinit var forwardBtn : MaterialButton
private lateinit var muteBtn : MaterialButton
private lateinit var rangeSlider : RangeSlider
private lateinit var fromTextInput : TextInputLayout
private lateinit var toTextInput : TextInputLayout
private lateinit var fromTextInput : EditText
private lateinit var toTextInput : EditText
private lateinit var cancelBtn : Button
private lateinit var okBtn : Button
private lateinit var forceKeyframes: MaterialSwitch
@ -85,7 +94,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
private lateinit var suggestedLabel : View
private lateinit var item: DownloadItem
private var timeSeconds by Delegates.notNull<Int>()
private var itemDurationTimestamp = 0L
private lateinit var selectedCuts: MutableList<String>
override fun onCreate(savedInstanceState: Bundle?) {
@ -121,7 +130,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
val frame = view.findViewById<MaterialCardView>(R.id.frame_layout)
val videoView = view.findViewById<PlayerView>(R.id.video_view)
videoView.player = player
timeSeconds = convertStringToTimestamp(item.duration)
itemDurationTimestamp = item.duration.convertToTimestamp()
if (chapters == null) chapters = emptyList()
//cut section
@ -131,10 +140,11 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
progress = view.findViewById(R.id.progress)
pauseBtn = view.findViewById(R.id.pause)
rewindBtn = view.findViewById(R.id.rewind)
forwardBtn = view.findViewById(R.id.forward)
muteBtn = view.findViewById(R.id.mute)
rangeSlider = view.findViewById(R.id.rangeSlider)
fromTextInput = view.findViewById(R.id.from_textinput)
toTextInput = view.findViewById(R.id.to_textinput)
fromTextInput = view.findViewById(R.id.from_textinput_edittext)
toTextInput = view.findViewById(R.id.to_textinput_edittext)
cancelBtn = view.findViewById(R.id.cancelButton)
okBtn = view.findViewById(R.id.okButton)
suggestedChips = view.findViewById(R.id.chapters)
@ -216,12 +226,12 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
videoProgress(player).collect { p ->
val currentTime = p.toStringDuration(Locale.US)
durationText.text = "$currentTime / ${item.duration}"
val startTimestamp = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
if (toTextInput.editText!!.text.isNotBlank()){
val endTimestamp = convertStringToTimestamp(toTextInput.editText!!.text.toString())
if (p >= endTimestamp){
val startTimestamp = fromTextInput.text.toString().convertToTimestamp()
if (toTextInput.text.isNotBlank()){
val endTimestamp = toTextInput.text.toString().convertToTimestamp()
if (p >= endTimestamp / 1000 || (!player.isPlaying && p >= endTimestamp / 1000 - 1)){
player.prepare()
player.seekTo((startTimestamp * 1000).toLong())
player.seekTo(startTimestamp)
}
}
@ -260,12 +270,22 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
rewindBtn.setOnClickListener {
try {
val seconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
player.seekTo((seconds * 1000).toLong())
val stmp = fromTextInput.text.toString().convertToTimestamp()
player.seekTo(stmp)
player.play()
}catch (ignored: Exception) {}
}
forwardBtn.setOnClickListener {
kotlin.runCatching {
val startTimestamp = fromTextInput.text.toString().convertToTimestamp()
var endTimestamp = toTextInput.text.toString().convertToTimestamp() - 1500
if (endTimestamp < startTimestamp) endTimestamp = startTimestamp
player.seekTo(endTimestamp)
player.play()
}
}
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = prefs.edit()
@ -279,12 +299,12 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
private fun initCutSection(){
fromTextInput.editText!!.setTextAndRecalculateWidth("0:00")
toTextInput.editText!!.setTextAndRecalculateWidth(item.duration)
fromTextInput.setTextAndRecalculateWidth("0:00")
toTextInput.setTextAndRecalculateWidth(item.duration)
rangeSlider.valueFrom = 0f
rangeSlider.valueTo = timeSeconds.toFloat()
rangeSlider.setValues(0F, timeSeconds.toFloat())
rangeSlider.valueTo = (itemDurationTimestamp / 1000).toFloat()
rangeSlider.setValues(0F, (itemDurationTimestamp / 1000).toFloat())
rangeSlider.setOnTouchListener { _, event -> // Handle touch events here
when (event.action) {
MotionEvent.ACTION_MOVE -> {
@ -298,37 +318,78 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
// Return 'false' to allow the event to continue propagating or 'true' to consume it
false
}
rangeSlider.addOnChangeListener { rangeSlider, fl, b ->
}
// fromTextInput.isFocusable = false
// fromTextInput.isClickable = true
// fromTextInput.setOnClickListener {
// val currentMilliseconds = (it as EditText).text.toString().convertToTimestamp()
// showTimestampBottomSheet(true, currentMilliseconds) { new ->
// var newTimestamp = new
// val endTimestamp = toTextInput.text.toString().convertToTimestamp()
// fromTextInput.setTextAndRecalculateWidth(newTimestamp.toStringTimeStamp())
//
// if (newTimestamp > itemDurationTimestamp) {
// newTimestamp = itemDurationTimestamp
// }
//
// rangeSlider.setValues((newTimestamp / 1000).toFloat(), (endTimestamp / 1000).toFloat())
// okBtn.isEnabled = newTimestamp != 0L || endTimestamp != itemDurationTimestamp
// try {
// player.seekTo(newTimestamp)
// player.play()
// }catch (ignored: Exception) {}
// }
// }
//
// toTextInput.isFocusable = false
// toTextInput.isClickable = true
// toTextInput.setOnClickListener {
// val currentMilliseconds = (it as EditText).text.toString().convertToTimestamp()
// showTimestampBottomSheet(false, currentMilliseconds) { new ->
// var newTimestamp = new
// val startTimestamp = fromTextInput.text.toString().convertToTimestamp()
//
// if (newTimestamp > itemDurationTimestamp) {
// newTimestamp = itemDurationTimestamp
// }
//
// toTextInput.setTextAndRecalculateWidth(newTimestamp.toStringTimeStamp())
// rangeSlider.setValues((startTimestamp / 1000).toFloat(), (newTimestamp / 1000).toFloat())
// okBtn.isEnabled = startTimestamp != 0L || newTimestamp != itemDurationTimestamp
// try {
// player.seekTo(newTimestamp - 1500)
// player.play()
// }catch (ignored: Exception) {}
// }
// }
fromTextInput.editText!!.setOnKeyListener(object : View.OnKeyListener {
fromTextInput.setOnKeyListener(object : View.OnKeyListener {
override fun onKey(p0: View?, keyCode: Int, event: KeyEvent?): Boolean {
if ((event!!.action == KeyEvent.ACTION_DOWN) &&
(keyCode == KeyEvent.KEYCODE_ENTER)) {
var startTimestamp = rangeSlider.valueFrom.toInt()
val endTimestamp = rangeSlider.valueTo.toInt()
fromTextInput.editText!!.clearFocus()
var seconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
val endSeconds = convertStringToTimestamp(toTextInput.editText!!.text.toString())
fromTextInput.clearFocus()
var timestamp = fromTextInput.text.toString().convertToTimestamp()
val endstamp = toTextInput.text.toString().convertToTimestamp()
if (seconds == 0) {
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
}else if (seconds > endSeconds){
if (timestamp == 0L) {
fromTextInput.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
}else if (timestamp > endstamp){
startTimestamp = 0
seconds = 0
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
timestamp = 0
fromTextInput.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
}else{
fromTextInput.setTextAndRecalculateWidth(fromTextInput.text.toString())
}
fromTextInput.editText!!.setTextAndRecalculateWidth(fromTextInput.editText!!.text.toString())
rangeSlider.setValues(seconds.toFloat(), endSeconds.toFloat())
okBtn.isEnabled = seconds != 0 || endSeconds != timeSeconds
rangeSlider.setValues((timestamp / 1000).toFloat(), (endstamp / 1000).toFloat())
okBtn.isEnabled = timestamp != 0L || endstamp != itemDurationTimestamp
try {
player.seekTo(seconds.toLong() * 1000)
player.seekTo(timestamp)
player.play()
}catch (ignored: Exception) {}
@ -339,36 +400,32 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
})
toTextInput.editText!!.setOnKeyListener(object : View.OnKeyListener {
toTextInput.setOnKeyListener(object : View.OnKeyListener {
override fun onKey(p0: View?, keyCode: Int, event: KeyEvent?): Boolean {
if ((event!!.action == KeyEvent.ACTION_DOWN) &&
(keyCode == KeyEvent.KEYCODE_ENTER)) {
var endTimestamp = (rangeSlider.valueTo.toInt() * timeSeconds) / 100
toTextInput.clearFocus()
val timestamp = fromTextInput.text.toString().convertToTimestamp()
var endstamp = toTextInput.text.toString().convertToTimestamp()
toTextInput.editText!!.clearFocus()
val startSeconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
var endSeconds = convertStringToTimestamp(toTextInput.editText!!.text.toString())
if (endSeconds > timeSeconds){
endTimestamp = timeSeconds
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
endSeconds = timeSeconds
if (endstamp > itemDurationTimestamp){
endstamp = itemDurationTimestamp
}
if (endstamp == 0L) {
endstamp = itemDurationTimestamp
}
if (endstamp <= timestamp){
endstamp = timestamp + 1000
}
if (endSeconds == 0) {
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
}else if (endSeconds <= rangeSlider.valueFrom.toInt()){
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
}
toTextInput.setTextAndRecalculateWidth(endstamp.toStringTimeStamp())
toTextInput.editText!!.setTextAndRecalculateWidth(toTextInput.editText!!.text.toString())
rangeSlider.setValues(startSeconds.toFloat(), endSeconds.toFloat())
okBtn.isEnabled = startSeconds != 0 || endSeconds != timeSeconds
rangeSlider.setValues((timestamp / 1000).toFloat(), (endstamp / 1000).toFloat())
okBtn.isEnabled = timestamp != 0L || endstamp != itemDurationTimestamp
try {
player.seekTo((endSeconds.toLong() - 2L) * 1000)
player.seekTo(endstamp - 1500)
player.play()
}catch (ignored: Exception) {}
@ -391,7 +448,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
okBtn.isEnabled = false
okBtn.setOnClickListener {
forceKeyframes.isVisible = true
val chip = createChip("${fromTextInput.editText!!.text}-${toTextInput.editText!!.text}")
val chip = createChip("${fromTextInput.text}-${toTextInput.text}")
chip.performClick()
cutSection.visibility = View.GONE
cutListSection.visibility = View.VISIBLE
@ -402,30 +459,30 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
private fun updateFromSlider(){
val values = rangeSlider.values
val startTimestamp = values[0].toInt()
val endTimestamp = values[1].toInt()
val startTimestamp = values[0].toLong() * 1000
val endTimestamp = values[1].toLong() * 1000
val startTimestampString = startTimestamp.toStringDuration(Locale.US)
val endTimestampString = endTimestamp.toStringDuration(Locale.US)
val startTimestampString = startTimestamp.toStringTimeStamp()
val endTimestampString = endTimestamp.toStringTimeStamp()
var draggedFromBeginning = true
if (toTextInput.editText!!.text.toString() != endTimestampString){
if (toTextInput.text.toString() != endTimestampString){
draggedFromBeginning = false
}
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestampString)
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestampString)
fromTextInput.setTextAndRecalculateWidth(startTimestampString)
toTextInput.setTextAndRecalculateWidth(endTimestampString)
okBtn.isEnabled = values[0] != 0F || values[1] != timeSeconds.toFloat()
okBtn.isEnabled = values[0] != 0F || values[1] != (itemDurationTimestamp / 1000).toFloat()
val startpos = (startTimestamp * 1000).toLong()
val startpos = startTimestamp * 1000
try {
if (draggedFromBeginning){
player.seekTo(startpos)
}else{
var endpos = (endTimestamp * 1000).toLong() - 1500
if (endpos < (startTimestamp * 1000).toLong()) endpos = startpos
var endpos = (endTimestamp * 1000) - 1500
if (endpos < (startTimestamp * 1000)) endpos = startpos
player.seekTo(endpos)
}
player.play()
@ -466,7 +523,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
newCutBtn.setOnClickListener {
cutSection.visibility = View.VISIBLE
cutListSection.visibility = View.GONE
rangeSlider.setValues(0F, timeSeconds.toFloat())
rangeSlider.setValues(0F, ( itemDurationTimestamp / 1000).toFloat())
player.seekTo(0)
suggestedChips.children.apply {
this.forEach {
@ -495,8 +552,8 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
}
private fun createChip(timestamp: String) : Chip {
val startTimestamp = convertStringToTimestamp(timestamp.split("-")[0].replace(";", ""))
val endTimestamp = convertStringToTimestamp(timestamp.split("-")[1].replace(";", ""))
val startTimestamp = timestamp.split("-")[0].replace(";", "").convertToTimestamp()
val endTimestamp = timestamp.split("-")[1].replace(";", "").convertToTimestamp()
val chip = layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
chip.text = timestamp
@ -508,9 +565,9 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
chip.setOnClickListener {
if (chip.isChecked) {
rangeSlider.setValues(startTimestamp.toFloat(), endTimestamp.toFloat())
rangeSlider.setValues((startTimestamp / 1000).toFloat(), (endTimestamp / 1000).toFloat())
player.prepare()
player.seekTo((startTimestamp * 1000).toLong())
player.seekTo(startTimestamp)
player.play()
}else {
player.seekTo(0)
@ -600,21 +657,95 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
}
}.flowOn(Dispatchers.Main)
private fun convertStringToTimestamp(duration: String): Int {
return try {
val timeArray = duration.split(":")
var timeSeconds = timeArray[timeArray.lastIndex].toInt()
var times = 60
for (i in timeArray.lastIndex - 1 downTo 0) {
timeSeconds += timeArray[i].toInt() * times
times *= 60
}
timeSeconds
}catch (e: Exception){
e.printStackTrace()
0
}
}
// private fun showTimestampBottomSheet(startTimestamp: Boolean, currentTimestamp: Long, onChange: (value: Long) -> Unit){
// val bottomSheet = BottomSheetDialog(requireContext())
// bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
// bottomSheet.setContentView(R.layout.adjust_cut_timestamp)
//
// val hours = bottomSheet.findViewById<NumberPicker>(R.id.hours)!!
// val minutes = bottomSheet.findViewById<NumberPicker>(R.id.minutes)!!
// val seconds = bottomSheet.findViewById<NumberPicker>(R.id.seconds)!!
// val milliseconds = bottomSheet.findViewById<NumberPicker>(R.id.milliseconds)!!
//
// val current = currentTimestamp.toTimePeriodsArray()
//
// val setLimits = fun() {
// val fromPeriods = fromTextInput.text.toString().convertToTimestamp().toTimePeriodsArray()
// val toPeriods = toTextInput.text.toString().convertToTimestamp().toTimePeriodsArray()
// val totalPeriods = itemDurationTimestamp.toTimePeriodsArray()
//
// if (startTimestamp){
// hours.minValue = 0
// hours.maxValue = toPeriods[Extensions.Period.HOUR]!!
//
// minutes.minValue = 0
// if (hours.maxValue > 0){
// minutes.maxValue = 59
// }else{
// minutes.maxValue = toPeriods[Extensions.Period.MINUTE]!!
// }
//
// seconds.minValue = 0
// if (minutes.maxValue > 0){
// seconds.maxValue = 59
// }else{
// seconds.maxValue = toPeriods[Extensions.Period.SECOND]!!
// }
// }else{
// hours.minValue = fromPeriods[Extensions.Period.HOUR]!!
// hours.maxValue = totalPeriods[Extensions.Period.HOUR]!!
//
// if (hours.minValue < 1){
// minutes.minValue = fromPeriods[Extensions.Period.MINUTE]!!
// minutes.maxValue = totalPeriods[Extensions.Period.MINUTE]!!
// }else{
// minutes.minValue = 0
// minutes.maxValue = 59
// }
//
// if (minutes.maxValue < 1){
// seconds.minValue = fromPeriods[Extensions.Period.SECOND]!!
// seconds.maxValue = totalPeriods[Extensions.Period.MINUTE]!!
// }else{
// seconds.minValue = 0
// seconds.maxValue = 59
// }
//
// }
// }
//
// setLimits()
// val timeSeconds = (itemDurationTimestamp / 1000).toInt()
// hours.value = current[Extensions.Period.HOUR]!!
// bottomSheet.findViewById<LinearLayout>(R.id.hours_container)?.isVisible = timeSeconds >= 3600
//
// minutes.value = current[Extensions.Period.MINUTE]!!
// bottomSheet.findViewById<LinearLayout>(R.id.minutes_container)?.isVisible = timeSeconds >= 60
//
// seconds.value = current[Extensions.Period.SECOND]!!
//
// milliseconds.minValue = 0
// milliseconds.maxValue = 9
// milliseconds.value = current[Extensions.Period.MILLISECOND]!!
//
// val getTimeStamp = fun() : Long {
// return "${hours.value}:${minutes.value}:${seconds.value}.${milliseconds.value}".convertToTimestamp()
// }
//
// val items = listOf(
// hours, minutes, seconds, milliseconds
// )
//
// items.forEach {
// it.setOnValueChangedListener { _, _, _ ->
// setLimits()
// onChange(getTimeStamp())
// }
// }
//
// bottomSheet.show()
// bottomSheet.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
// }
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)

View file

@ -6,6 +6,7 @@ import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
@ -17,6 +18,7 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import androidx.fragment.app.Fragment
@ -45,6 +47,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Locale
class DownloadAudioFragment(private var resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "", private var nonSpecific: Boolean = false) : Fragment(), GUISync {

View file

@ -553,10 +553,8 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
}else{
//open multi download card instead
if (activity is ShareActivity){
val preferredType = DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!)
findNavController().navigate(R.id.action_downloadBottomSheetDialog_to_selectPlaylistItemsDialog, bundleOf(
Pair("results", result),
Pair("type", preferredType),
Pair("resultIDs", result.map { it!!.id }.toLongArray()),
))
}else{
dismiss()

View file

@ -91,7 +91,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
preferences.edit().putString("lastCommandTemplateUsed", downloadItem.format.format_note).apply()
if (!Patterns.WEB_URL.matcher(downloadItem.url).matches()){
if (!Patterns.WEB_URL.matcher(downloadItem.url).matches() && downloadItem.url.endsWith(".txt")){
downloadItem.format = downloadViewModel.generateCommandFormat(CommandTemplate(0,"txt", "-a \"${downloadItem.url}\"", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false))
downloadItem.url = ""
}

View file

@ -3,15 +3,20 @@ package com.deniscerri.ytdl.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.content.SharedPreferences
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.os.IBinder
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
@ -21,7 +26,11 @@ import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.core.view.forEach
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
@ -36,6 +45,7 @@ import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.services.ProcessDownloadsInBackgroundService
import com.deniscerri.ytdl.ui.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.FileUtil
@ -49,12 +59,17 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
@ -75,6 +90,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var sharedPreferences: SharedPreferences
private lateinit var currentDownloadIDs: List<Long>
private var processingDownloadsJobID: Long = 0
private var processingItemsCount : Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -86,6 +103,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
infoUtil = InfoUtil(requireContext())
currentDownloadIDs = arguments?.getLongArray("currentDownloadIDs")?.toList() ?: listOf()
processingDownloadsJobID = arguments?.getLong("processingDownloadsJobID") ?: 0
processingItemsCount = currentDownloadIDs.size
}
@SuppressLint("RestrictedApi", "NotifyDataSetChanged")
@ -131,40 +150,83 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
val download = view.findViewById<Button>(R.id.bottomsheet_download_button)
val progressBar = view.findViewById<LinearProgressIndicator>(R.id.loadingItemsProgress)
filesize = view.findViewById(R.id.filesize)
count = view.findViewById(R.id.count)
scheduleBtn.setOnClickListener{
UiUtil.showDatePicker(parentFragmentManager) { cal ->
scheduleBtn.isEnabled = false
download.isEnabled = false
fun initSchedule() {
UiUtil.showDatePicker(parentFragmentManager) { cal ->
scheduleBtn.isEnabled = false
download.isEnabled = false
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
}
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.downloadProcessingDownloads(cal.timeInMillis)
val jobData = withContext(Dispatchers.IO){
downloadViewModel.getLoadingProcessingDownloadJob(processingDownloadsJobID)
}
jobData?.job?.cancel(CancellationException())
downloadProcessDownloadsInBackground(jobData, cal.timeInMillis)
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(cal.timeInMillis)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
dismiss()
}
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(cal.timeInMillis)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
dismiss()
}
}
if (progressBar.isVisible){
val continueInBackgroundSnackBar = Snackbar.make(view, R.string.process_downloads_background, Snackbar.LENGTH_LONG)
val snackbarView: View = continueInBackgroundSnackBar.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
continueInBackgroundSnackBar.setAction(R.string.ok) {
initSchedule()
}
continueInBackgroundSnackBar.show()
}else{
initSchedule()
}
}
download!!.setOnClickListener {
scheduleBtn.isEnabled = false
download.isEnabled = false
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.downloadProcessingDownloads()
fun initDownload() {
scheduleBtn.isEnabled = false
download.isEnabled = false
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
}
val jobData = withContext(Dispatchers.IO){
downloadViewModel.getLoadingProcessingDownloadJob(processingDownloadsJobID)
}
jobData?.job?.cancel(CancellationException())
downloadProcessDownloadsInBackground(jobData)
dismiss()
}
dismiss()
}
if (progressBar.isVisible){
val continueInBackgroundSnackBar = Snackbar.make(view, R.string.process_downloads_background, Snackbar.LENGTH_LONG)
val snackbarView: View = continueInBackgroundSnackBar.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
continueInBackgroundSnackBar.setAction(R.string.ok) {
initDownload()
}
continueInBackgroundSnackBar.show()
}else{
initDownload()
}
}
download.setOnLongClickListener {
@ -206,8 +268,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
withContext(Dispatchers.IO){
downloadViewModel.continueUpdatingFormatsOnBackground()
}
dismiss()
}
dismiss()
}
}
@ -537,45 +599,55 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
lifecycleScope.launch {
downloadViewModel.processingDownloads.collectLatest {
count.text = "${it.size} ${getString(R.string.selected)}"
listAdapter.submitList(it)
withContext(Dispatchers.Main){
updateFileSize(it.map { it2 -> it2.format.filesize })
}
downloadViewModel.loadingProcessingDownloadsJobs.collectLatest { jobs ->
processingItemsCount = jobs.firstOrNull { j -> j.jobID == processingDownloadsJobID }?.itemIDs?.size ?: currentDownloadIDs.size
lifecycleScope.launch {
downloadViewModel.processingDownloads.collectLatest {
count.text = "${it.size} ${getString(R.string.selected)}"
if (it.isNotEmpty()){
if (it.all { it2 -> it2.type == it[0].type }) {
withContext(Dispatchers.Main) {
bottomAppBar.menu[1].icon?.alpha = 255
if (it[0].type != DownloadViewModel.Type.command) {
bottomAppBar.menu[3].icon?.alpha = 255
val loading = it.size != processingItemsCount
progressBar.isVisible = loading
bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading }
listAdapter.submitList(it)
withContext(Dispatchers.Main){
updateFileSize(it.map { it2 -> it2.format.filesize })
}
if (it.isNotEmpty()){
if (it.all { it2 -> it2.type == it[0].type }) {
withContext(Dispatchers.Main) {
bottomAppBar.menu[1].icon?.alpha = 255
if (it[0].type != DownloadViewModel.Type.command) {
bottomAppBar.menu[3].icon?.alpha = 255
}
}
} else {
bottomAppBar.menu[1].icon?.alpha = 30
bottomAppBar.menu[3].icon?.alpha = 30
}
val type = it.first().type
when(type){
DownloadViewModel.Type.audio -> {
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
}
DownloadViewModel.Type.video -> {
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
}
DownloadViewModel.Type.command -> {
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
}
else -> {}
}
}
} else {
bottomAppBar.menu[1].icon?.alpha = 30
bottomAppBar.menu[3].icon?.alpha = 30
}
val type = it.first().type
when(type){
DownloadViewModel.Type.audio -> {
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
}
DownloadViewModel.Type.video -> {
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
}
DownloadViewModel.Type.command -> {
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
}
else -> {}
}
}
}
}
}
@ -700,15 +772,14 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
UiUtil.showGenericDeleteDialog(requireContext(), deletedItem.title){
lifecycleScope.launch {
val count = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloadsCount()
}
processingItemsCount--
downloadViewModel.deleteDownload(id)
if (count > 1){
if (processingItemsCount > 0){
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO) {
processingItemsCount++
downloadViewModel.insert(deletedItem)
}
}.show()
@ -736,9 +807,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
override fun onDismiss(dialog: DialogInterface) {
requireActivity().lifecycleScope.launch {
downloadViewModel.deleteProcessing()
}
downloadViewModel.cancelLoadingProcessingDownloads(processingDownloadsJobID)
downloadViewModel.deleteProcessing()
super.onDismiss(dialog)
}
@ -757,16 +827,16 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val deletedItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
val count = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloadsCount()
}
processingItemsCount--
withContext(Dispatchers.IO){
downloadViewModel.deleteDownload(deletedItem.id)
}
if (count > 1) {
if (processingItemsCount > 0) {
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
processingItemsCount++
downloadViewModel.insert(deletedItem)
}.show()
}else{
@ -819,5 +889,17 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
}
private fun downloadProcessDownloadsInBackground(jobData: DownloadViewModel.ProcessingItemsJob?, timeInMillis: Long = 0){
Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent ->
jobData?.apply {
intent.putExtra("itemType", jobData.itemType)
intent.putExtra("itemIDs", jobData.itemIDs.toLongArray())
intent.putExtra("timeInMillis", timeInMillis)
}
requireActivity().startService(intent)
}
}
}

View file

@ -0,0 +1,174 @@
package com.deniscerri.ytdl.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.DialogInterface
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.View
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
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.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
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.elevation.SurfaceColors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class DownloadsAlreadyExistDialog : BottomSheetDialogFragment(), AlreadyExistsAdapter.OnItemClickListener {
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
private lateinit var resultViewModel : ResultViewModel
private lateinit var historyViewModel : HistoryViewModel
private var duplicateIDs : MutableList<AlreadyExistsIDs> = mutableListOf()
private lateinit var duplicates: MutableList<AlreadyExistsItem>
private lateinit var preferences: SharedPreferences
private lateinit var adapter: AlreadyExistsAdapter
private lateinit var recyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity = getActivity()
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java]
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
kotlin.runCatching {
duplicateIDs = if (Build.VERSION.SDK_INT >= 33){
arguments?.getParcelableArrayList("duplicates", AlreadyExistsIDs::class.java)!!.toMutableList()
}else{
arguments?.getParcelableArrayList<AlreadyExistsIDs>("duplicates")!!.toMutableList()
}
if (duplicateIDs.isEmpty()){
dismiss()
}
}.onFailure {
dismiss()
}
}
@SuppressLint("RestrictedApi")
override fun setupDialog(dialog: Dialog, style: Int) {
super.setupDialog(dialog, style)
val view = requireActivity().layoutInflater.inflate(R.layout.fragment_already_exists_dialog, null)
dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
dialog.setOnShowListener {
val behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
if(resources.getBoolean(R.bool.isTablet) || resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.peekHeight = displayMetrics.heightPixels
}
}
adapter = AlreadyExistsAdapter(this, requireActivity())
recyclerView = view.findViewById(R.id.downloadMultipleRecyclerview)
recyclerView.adapter = adapter
recyclerView.enableFastScroll()
runBlocking {
val items = withContext(Dispatchers.IO){
downloadViewModel.getAllByIDs(duplicateIDs.map { it.downloadItemID })
}
duplicates = items.map { item -> AlreadyExistsItem(item, duplicateIDs.firstOrNull { it.downloadItemID == item.id }?.historyItemID) }.toMutableList()
adapter.submitList(duplicates.toList())
}
view.findViewById<MaterialButton>(R.id.bottomsheet_download_button).setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteProcessing()
val items = duplicates.map { it.downloadItem }
items.forEach { it.id = 0 }
downloadViewModel.queueDownloads(items, true)
withContext(Dispatchers.Main){
dismiss()
}
}
}
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteProcessing()
downloadViewModel.deleteAllWithID(duplicates.map { it.downloadItem.id })
}
}
override fun onEditItem(alreadyExistsItem: AlreadyExistsItem, position: Int) {
val resultItem = downloadViewModel.createResultItemFromDownload(alreadyExistsItem.downloadItem)
val onItemUpdated = object: ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
override fun onDownloadItemUpdate(
resultItemID: Long,
item: DownloadItem
) {
val currentIndex = duplicates.indexOf(alreadyExistsItem)
val current = duplicates[currentIndex]
duplicates[currentIndex] = AlreadyExistsItem(item, current.historyID)
adapter.submitList(duplicates)
adapter.notifyItemChanged(position)
}
}
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, alreadyExistsItem.downloadItem, onItemUpdated)
bottomSheet.show(requireActivity().supportFragmentManager, "configureDownloadSingleSheet")
}
override fun onDeleteItem(alreadyExistsItem: AlreadyExistsItem, position: Int) {
UiUtil.showGenericDeleteDialog(requireContext(), alreadyExistsItem.downloadItem.title) {
if (alreadyExistsItem.historyID == null) {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteDownload(alreadyExistsItem.downloadItem.id)
}
}
duplicates.remove(alreadyExistsItem)
if (duplicates.isEmpty()) {
dismiss()
}
adapter.submitList(duplicates)
}
}
override fun onShowHistoryItem(historyItemID: Long) {
lifecycleScope.launch {
val historyItem = withContext(Dispatchers.IO){
downloadViewModel.getHistoryItemById(historyItemID)
}
UiUtil.showHistoryItemDetailsCard(historyItem, requireActivity(), isPresent = true,
removeItem = { item, deleteFile ->
historyViewModel.delete(item, deleteFile)
},
redownloadItem = { },
redownloadShowDownloadCard = {}
)
}
}
}

View file

@ -253,13 +253,14 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
.sumOf { itt -> itt.filesize }
}
}
hasGenericFormats = false
hasGenericFormats = formatCollection.size == 0
withContext(Dispatchers.Main){
shimmers.visibility = View.GONE
shimmers.stopShimmer()
filterBtn.isVisible = true
filterBtn.isVisible = !hasGenericFormats
addFormatsToView()
refreshBtn.visibility = View.GONE
refreshBtn.isVisible = hasGenericFormats
refreshBtn.isEnabled = hasGenericFormats
formatListLinearLayout.visibility = View.VISIBLE
}
}catch (e: Exception){

View file

@ -55,6 +55,7 @@ import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.hamcrest.core.Every
import java.text.SimpleDateFormat
import java.time.Month
import java.util.Calendar
@ -84,6 +85,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var title: TextInputLayout
private lateinit var url: TextInputLayout
private lateinit var everyCat: AutoCompleteTextView
private lateinit var categoryAdapter: ArrayAdapter<String>
private lateinit var everyTime: TextInputLayout
private lateinit var weekDays: ChipGroup
private lateinit var everyMonthDay: TextInputLayout
@ -291,7 +293,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
everyNr.editText?.doAfterTextChanged { checkIfValid() }
val cats = ObserveSourcesRepository.everyCategoryName.map { getString(it.value) }
val adapter = ArrayAdapter(requireActivity(),android.R.layout.simple_dropdown_item_1line, cats)
categoryAdapter = ArrayAdapter(requireActivity(),android.R.layout.simple_dropdown_item_1line, cats)
everyCat.doAfterTextChanged {
everyTime.isVisible = it.toString() != cats[0]
weekDays.isVisible = it.toString() == cats[2]
@ -299,7 +301,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
startTime.isVisible = !startMonth.isVisible
everyMonthDay.isVisible = startMonth.isVisible
}
everyCat.setAdapter(adapter)
everyCat.setAdapter(categoryAdapter)
if (currentItem != null){
val idx = ObserveSourcesRepository.EveryCategory.values().indexOf(currentItem!!.everyCategory)
everyCat.setText(cats[idx], false)
@ -473,6 +475,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
okButton.setOnClickListener {
lifecycleScope.launch {
val item: DownloadItem = getDownloadItem()
item.url = url.editText!!.text.toString()
var ends = 0L
if(endsOn.isChecked){
@ -484,7 +487,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
endsAfterCount = endsAfterNr.editText!!.text.toString().toInt()
}
val category = ObserveSourcesRepository.EveryCategory.values()[adapter.getPosition(everyCat.text.toString())]
val category = ObserveSourcesRepository.EveryCategory.values()[categoryAdapter.getPosition(everyCat.text.toString())]
val observeItem = ObserveSourcesItem(
id = currentItem?.id ?: 0,
@ -493,7 +496,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
downloadItemTemplate = item,
everyNr = everyNr.editText!!.text.toString().toInt(),
everyCategory = category,
everyTime = everyTime.tag as Long,
everyTime = (everyTime.tag ?: 0L) as Long,
weeklyConfig = if (category == ObserveSourcesRepository.EveryCategory.WEEK) {
ObserveSourcesWeeklyConfig(
weekDays = weekDays.children.filter { (it as Chip).isChecked }.map { it.tag.toString().toInt() }.toList()
@ -534,16 +537,31 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
}
private fun checkIfValid(){
var valid = title.editText?.text?.isNotBlank() == true
&& url.editText?.text?.isNotBlank() == true
&& everyNr.editText?.text?.isNotBlank() == true
&& everyTime.editText?.text?.isNotBlank() == true
&& startTime.editText?.text?.isNotBlank() == true
val title = title.editText?.text?.isNotBlank() == true
val url = url.editText?.text?.isNotBlank() == true
val category = ObserveSourcesRepository.EveryCategory.values()[categoryAdapter.getPosition(everyCat.text.toString())]
if (endsOn.isChecked && endsOnTime.editText?.text?.isBlank() == true) valid = false
else if (endsAfter.isChecked && endsAfterNr.editText?.text?.isBlank() == true) valid = false
val everyNr = everyNr.editText?.text?.isNotBlank() == true
val everyTime = everyTime.editText?.text?.isNotBlank() == true
okButton.isEnabled = valid
val every = when(category){
ObserveSourcesRepository.EveryCategory.HOUR -> {
everyNr
}
else -> {
everyNr && everyTime
}
}
val ends = if (endsOn.isChecked){
endsOnTime.editText?.text?.isNotBlank() == true
}else if (endsAfter.isChecked){
endsAfterNr.editText?.text?.isNotBlank() == true
}else{
true
}
okButton.isEnabled = title && url && every && ends
}
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{

View file

@ -11,9 +11,12 @@ import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -31,6 +34,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -49,28 +53,23 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
private lateinit var count: TextView
private lateinit var selectBetween: MenuItem
private lateinit var items: List<ResultItem?>
private lateinit var resultItemIDs: List<Long>
private lateinit var itemURLs: List<String>
private lateinit var type: DownloadViewModel.Type
private var items = listOf<ResultItem>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
if (Build.VERSION.SDK_INT >= 33){
arguments?.getParcelableArrayList("results", ResultItem::class.java)
}else{
arguments?.getParcelableArrayList<ResultItem>("results")
}.apply {
arguments?.getLongArray("resultIDs").apply {
if (this == null){
dismiss()
return
}else{
items = this
resultItemIDs = this.toList()
}
}
type = arguments?.getSerializable("type") as DownloadViewModel.Type
}
@SuppressLint("RestrictedApi", "NotifyDataSetChanged")
@ -90,6 +89,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
}
}
val progress = view.findViewById<LinearProgressIndicator>(R.id.loadingItemsProgress)
listAdapter =
PlaylistAdapter(
@ -101,7 +101,6 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = listAdapter
recyclerView.enableFastScroll()
listAdapter.submitList(items)
count = view.findViewById(R.id.count)
count.text = "0 ${resources.getString(R.string.selected)}"
@ -159,7 +158,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
fromTextInput.isEnabled = true
toTextInput.isEnabled = true
ok.isEnabled = true
count.text = resources.getString(R.string.all_items_selected)
count.text = "(${listAdapter.getCheckedItems().size}) ${resources.getString(R.string.all_items_selected)} "
}else{
reset()
fromTextInput.isEnabled = true
@ -183,25 +182,15 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
withContext(Dispatchers.Main){
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf(
Pair("result", resultItem),
Pair("type", downloadViewModel.getDownloadType(type, resultItem.url)),
Pair("type", downloadViewModel.getDownloadType(url = resultItem.url)),
))
}
}else{
val downloadItems = mutableListOf<DownloadItem>()
checkedResultItems.forEach { c ->
c!!.id = 0
val i = downloadViewModel.createDownloadItemFromResult(
result = c, givenType = type)
if (type == DownloadViewModel.Type.command){
i.format = downloadViewModel.getLatestCommandTemplateAsFormat()
}
downloadItems.add(i)
}
downloadViewModel.insertToProcessing(downloadItems)
val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(checkedResultItems.map { it!!.id })
withContext(Dispatchers.Main){
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadMultipleBottomSheetDialog)
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadMultipleBottomSheetDialog,
bundleOf(Pair("processingDownloadsJobID", jobID))
)
}
}
@ -218,7 +207,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
listAdapter.invertSelected(items)
val checkedItems = listAdapter.getCheckedItems()
if (checkedItems.size == items.size){
count.text = resources.getString(R.string.all_items_selected)
count.text = "(${listAdapter.getCheckedItems().size}) ${resources.getString(R.string.all_items_selected)} "
}else{
count.text = "${checkedItems.size} ${resources.getString(R.string.selected)}"
}
@ -245,7 +234,23 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
}
selectBetween = bottomAppBar.menu.findItem(R.id.select_between)
itemURLs = items.map { it!!.url }
lifecycleScope.launch {
resultViewModel.items.map { items -> items.filter { resultItemIDs.contains(it.id) } }.observe(this@SelectPlaylistItemsDialog) {
val isLoading = it.size != resultItemIDs.size
progress.isVisible = isLoading
listAdapter.submitList(it)
recyclerView.suppressLayout(isLoading)
bottomAppBar.menu.children.forEach { c -> c.isEnabled = !isLoading }
ok.isEnabled = !isLoading
checkAll.isEnabled = !isLoading
fromTextInput.isEnabled = !isLoading
toTextInput.isEnabled = !isLoading
items = it
itemURLs = items.map { itm -> itm.url }
}
}
}
@ -263,7 +268,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
override fun onCardSelect(itemURL: String, isChecked: Boolean, checkedItems: List<String>) {
if (checkedItems.size == items.size){
count.text = resources.getString(R.string.all_items_selected)
count.text = "(${listAdapter.getCheckedItems().size}) ${resources.getString(R.string.all_items_selected)} "
}else{
count.text = "${checkedItems.size} ${resources.getString(R.string.selected)}"
}

View file

@ -40,7 +40,9 @@ import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -251,19 +253,18 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
R.id.redownload -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
if (preferences.getBoolean("download_card", true)){
withContext(Dispatchers.IO){
downloadViewModel.addDownloadsToProcessing(selectedObjects)
val showDownloadCard = preferences.getBoolean("download_card", true)
if (showDownloadCard) {
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
downloadViewModel.turnDownloadItemsToProcessingDownloads(selectedObjects)
}
withContext(Dispatchers.Main){
val bundle = Bundle()
bundle.putLongArray("currentDownloadIDs", selectedObjects.toLongArray())
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2,bundle)
}
}else{
withContext(Dispatchers.IO) {
downloadViewModel.reQueueDownloadItems(selectedObjects)
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundle)
}
}else {
downloadViewModel.reQueueDownloadItems(selectedObjects)
}
adapter.clearCheckedItems()
@ -275,7 +276,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
}
R.id.select_all -> {
adapter.checkAll()
mode?.title = getString(R.string.all_items_selected)
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {

View file

@ -1,8 +1,13 @@
package com.deniscerri.ytdl.ui.downloads
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.content.SharedPreferences
import android.os.Bundle
import android.os.IBinder
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
@ -10,9 +15,11 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
@ -22,6 +29,7 @@ import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.services.ProcessDownloadsInBackgroundService
import com.deniscerri.ytdl.util.Extensions.createBadge
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil
@ -116,9 +124,24 @@ class DownloadQueueMainFragment : Fragment(){
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 {
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(reconfigureID)
}
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
Pair("downloadItem", item),
Pair("result", downloadViewModel.createResultItemFromDownload(item)),
Pair("type", item.type)
)
)
}
}
}, 200)
}
@ -243,14 +266,37 @@ class DownloadQueueMainFragment : Fragment(){
workManager.cancelAllWorkByTag("download")
lifecycleScope.launch {
val notificationUtil = NotificationUtil(requireContext())
downloadViewModel.cancelActiveQueued()
cancelBackgroundProcessingDownloads()
val activeAndQueued = withContext(Dispatchers.IO){
downloadViewModel.getActiveAndQueuedDownloadIDs()
}
downloadViewModel.cancelActiveQueued()
activeAndQueued.forEach { id ->
YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id.toInt())
}
}
}
private fun cancelBackgroundProcessingDownloads(){
val connection = object : ServiceConnection {
private lateinit var mService: ProcessDownloadsInBackgroundService
private var mBound: Boolean = false
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as ProcessDownloadsInBackgroundService.LocalBinder
mService = binder.service
mBound = true
mService.cancelAllProcessingJobs()
}
override fun onServiceDisconnected(arg0: ComponentName) {
mBound = false
}
}
Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent ->
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
}

View file

@ -41,7 +41,9 @@ import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -243,20 +245,18 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
R.id.redownload -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
if (preferences.getBoolean("download_card", true)){
withContext(Dispatchers.IO){
downloadViewModel.addDownloadsToProcessing(selectedObjects)
val showDownloadCard = preferences.getBoolean("download_card", true)
if (showDownloadCard) {
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
downloadViewModel.turnDownloadItemsToProcessingDownloads(selectedObjects)
}
withContext(Dispatchers.Main){
val bundle = Bundle()
bundle.putLongArray("currentDownloadIDs", selectedObjects.toLongArray())
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2,bundle)
}
}else{
withContext(Dispatchers.IO) {
downloadViewModel.reQueueDownloadItems(selectedObjects)
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundle)
}
}else {
downloadViewModel.reQueueDownloadItems(selectedObjects)
}
adapter.clearCheckedItems()
@ -266,7 +266,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
}
R.id.select_all -> {
adapter.checkAll()
mode?.title = getString(R.string.all_items_selected)
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {

View file

@ -183,22 +183,14 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
lifecycleScope.launch{
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.downloadItems.isNotEmpty() || res.historyItems.isNotEmpty()) {
if (res.isNotEmpty()){
withContext(Dispatchers.Main){
kotlin.runCatching {
UiUtil.handleExistingDownloadsResponse(
requireActivity(),
requireActivity().lifecycleScope,
requireActivity().supportFragmentManager,
res,
downloadViewModel,
historyViewModel)
}
val bundle = bundleOf(
Pair("duplicates", res)
)
findNavController().navigate(R.id.action_historyFragment_to_downloadsAlreadyExistDialog, bundle)
}
downloadViewModel.alreadyExistsUiState.value = DownloadViewModel.AlreadyExistsUIState(
mutableListOf(),
mutableListOf()
)
downloadViewModel.alreadyExistsUiState.value = mutableListOf()
}
}
}
@ -558,7 +550,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyAdapter?.checkAll(historyList)
selectedObjects.clear()
historyList?.forEach { selectedObjects.add(it!!) }
mode?.title = getString(R.string.all_items_selected)
mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {

View file

@ -238,7 +238,7 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
}
R.id.select_all -> {
adapter.checkAll()
mode?.title = getString(R.string.all_items_selected)
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {

View file

@ -40,7 +40,9 @@ import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -240,19 +242,18 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
adapter.clearCheckedItems()
if (preferences.getBoolean("download_card", true)){
withContext(Dispatchers.IO){
downloadViewModel.addDownloadsToProcessing(selectedObjects)
val showDownloadCard = preferences.getBoolean("download_card", true)
if (showDownloadCard) {
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
downloadViewModel.turnDownloadItemsToProcessingDownloads(selectedObjects)
}
withContext(Dispatchers.Main){
val bundle = Bundle()
bundle.putLongArray("currentDownloadIDs", selectedObjects.toLongArray())
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2,bundle)
}
}else{
withContext(Dispatchers.IO) {
downloadViewModel.reQueueDownloadItems(selectedObjects)
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundle)
}
}else {
downloadViewModel.reQueueDownloadItems(selectedObjects)
}
actionMode?.finish()
@ -261,7 +262,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
}
R.id.select_all -> {
adapter.checkAll()
mode?.title = getString(R.string.all_items_selected)
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {

View file

@ -116,8 +116,7 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
downloadViewModel.getItemByID(itemID)
}
withContext(Dispatchers.IO){
item.downloadStartTime = 0
downloadViewModel.queueDownloads(listOf(item), true)
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(listOf(item.id))
}
}.onFailure {
Toast.makeText(requireContext(), it.message, Toast.LENGTH_LONG).show()
@ -125,7 +124,7 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
}
}
override fun onCardClick(itemID: Long) {
override fun onCardClick(itemID: Long, position: Int) {
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
@ -163,6 +162,7 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(downloadItem))
adapter.notifyItemChanged(position)
}
}
}
@ -221,7 +221,7 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.queued_menu_context, menu)
mode!!.menuInflater.inflate(R.menu.scheduled_downloads_menu_context, menu)
mode.title = "${adapter.getSelectedObjectsCount(totalSize)} ${getString(R.string.selected)}"
return true
}
@ -284,7 +284,7 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
}
R.id.select_all -> {
adapter.checkAll()
mode?.title = getString(R.string.all_items_selected)
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {
@ -383,13 +383,13 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
R.attr.colorOnSurfaceInverse,Color.TRANSPARENT
)
)
.addSwipeRightActionIcon(R.drawable.ic_refresh)
.addSwipeRightActionIcon(R.drawable.ic_downloads)
.create()
.decorate()
super.onChildDraw(
c,
recyclerView,
viewHolder!!,
viewHolder,
dX,
dY,
actionState,

View file

@ -351,7 +351,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
templatesAdapter.checkAll(templatesList)
selectedObjects.clear()
templatesList.forEach { selectedObjects.add(it) }
mode?.title = getString(R.string.all_items_selected)
mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {

View file

@ -193,7 +193,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
downloadLogAdapter.checkAll(items)
selectedObjects.clear()
items.forEach { selectedObjects.add(it) }
mode?.title = getString(R.string.all_items_selected)
mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {

View file

@ -1,8 +1,11 @@
package com.deniscerri.ytdl.ui.more.settings
import android.content.DialogInterface
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.edit
import androidx.preference.ListPreference
import androidx.preference.Preference
@ -13,11 +16,10 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.DownloadWorker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.materialswitch.MaterialSwitch
import java.util.Calendar
import java.util.concurrent.TimeUnit
@ -25,6 +27,8 @@ import java.util.concurrent.TimeUnit
class DownloadSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.downloads
private lateinit var archivePath: Preference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.downloading_preferences, rootKey)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
@ -47,6 +51,25 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
true
}
val preventDuplicateDownloads = findPreference<ListPreference>("prevent_duplicate_downloads")
preventDuplicateDownloads?.setOnPreferenceChangeListener { _, newValue ->
archivePath.isVisible = newValue == "download_archive"
true
}
archivePath = findPreference("download_archive_path")!!
archivePath.summary = FileUtil.getDownloadArchivePath(requireContext())
archivePath.isVisible = preferences.getString("prevent_duplicate_downloads", "") == "download_archive"
archivePath.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
archivePathResultLauncher.launch(intent)
true
}
val useScheduler = findPreference<SwitchPreferenceCompat>("use_scheduler")
val scheduleStart = findPreference<Preference>("schedule_start")
scheduleStart?.summary = preferences.getString("schedule_start", "00:00")
@ -55,8 +78,17 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
val scheduler = AlarmScheduler(requireContext())
useScheduler?.setOnPreferenceChangeListener { preference, newValue ->
var allowChange = true
if (newValue as Boolean){
scheduler.schedule()
if (!scheduler.canSchedule() && Build.VERSION.SDK_INT >= 31){
Intent().also { intent ->
intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
requireContext().startActivity(intent)
}
allowChange = false
}else{
scheduler.schedule()
}
}else{
scheduler.cancel()
//start worker if there are leftover downloads waiting for scheduler
@ -72,7 +104,7 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
workRequest.build()
)
}
true
allowChange
}
scheduleStart?.setOnPreferenceClickListener {
@ -102,4 +134,25 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
}
}
private var archivePathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
val path = result.data!!.data.toString()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = preferences.edit()
editor.putString("download_archive_path", path)
editor.apply()
archivePath.summary = FileUtil.getDownloadArchivePath(requireContext())
}
}
}

View file

@ -1,23 +1,37 @@
package com.deniscerri.ytdl.ui.more.settings
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.graphics.Typeface
import android.os.Bundle
import android.widget.LinearLayout
import android.widget.TextView
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.ui.adapter.SortableTextItemAdapter
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class ProcessingSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.processing
@SuppressLint("RestrictedApi")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.processing_preferences, rootKey)
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity())
val editor = prefs.edit()
val preferredFormatID : EditTextPreference? = findPreference("format_id")
val preferredFormatIDAudio : EditTextPreference? = findPreference("format_id_audio")
val subtitleLanguages : Preference? = findPreference("subs_lang")
val formatImportanceAudio: Preference? = findPreference("format_importance_audio")
val formatImportanceVideo: Preference? = findPreference("format_importance_video")
preferredFormatID?.title = "${getString(R.string.preferred_format_id)} [${getString(R.string.video)}]"
preferredFormatID?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
@ -34,6 +48,119 @@ class ProcessingSettingsFragment : BaseSettingsFragment() {
}
true
}
formatImportanceAudio?.apply {
val items = requireContext().getStringArray(R.array.format_importance_audio)
val itemValues = requireContext().getStringArray(R.array.format_importance_audio_values).toSet()
val prefVideo = prefs.getString("format_importance_audio", itemValues.joinToString(","))!!
summary = prefVideo.split(",").mapIndexed { index, s -> "${index + 1}. ${items[itemValues.indexOf(s)]}" }.joinToString("\n")
setOnPreferenceClickListener {
val pref = prefs.getString("format_importance_audio", itemValues.joinToString(","))!!
val itms = pref.split(",").map {
Pair(it, items[itemValues.indexOf(it)])
}.toMutableList()
showFormatImportanceDialog(getString(R.string.format_importance_audio), itms) { new ->
editor.putString("format_importance_audio", new.joinToString(",") { it.first }).apply()
formatImportanceAudio.summary = new.map { it.second }.mapIndexed { index, s -> "${index + 1}. $s" }.joinToString("\n")
}
true
}
}
formatImportanceVideo?.apply {
val items = requireContext().getStringArray(R.array.format_importance_video)
val itemValues = requireContext().getStringArray(R.array.format_importance_video_values).toSet()
val prefVideo = prefs.getString("format_importance_video", itemValues.joinToString(","))!!
summary = prefVideo.split(",").mapIndexed { index, s -> "${index + 1}. ${items[itemValues.indexOf(s)]}" }.joinToString("\n")
setOnPreferenceClickListener {
val pref = prefs.getString("format_importance_video", itemValues.joinToString(","))!!
val itms = pref.split(",").map {
Pair(it, items[itemValues.indexOf(it)])
}.toMutableList()
showFormatImportanceDialog(getString(R.string.format_importance_video), itms) {new ->
editor.putString("format_importance_video", new.joinToString(",") { it.first }).apply()
formatImportanceVideo.summary = new.map { it.second }.mapIndexed { index, s -> "${index + 1}. $s" }.joinToString("\n")
}
true
}
}
}
private fun showFormatImportanceDialog(t: String, items: MutableList<Pair<String, String>>, onChange: (items: List<Pair<String, String>>) -> Unit){
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(t)
val adapter = SortableTextItemAdapter(items)
val itemTouchCallback = object : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
return makeMovementFlags(dragFlags, 0)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val itemToMove = adapter.items[viewHolder.absoluteAdapterPosition]
adapter.items.remove(itemToMove)
adapter.items.add(target.absoluteAdapterPosition, itemToMove)
adapter.notifyItemMoved(
viewHolder.absoluteAdapterPosition,
target.absoluteAdapterPosition
)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// do nothing
}
}
val linear = LinearLayout(requireActivity())
linear.orientation = LinearLayout.VERTICAL
val note = TextView(requireActivity())
note.text = getString(R.string.format_importance_note)
note.textSize = 16f
note.setTypeface(note.typeface, Typeface.BOLD)
note.setPadding(20,20,20,20)
linear.addView(note)
val recycler = RecyclerView(requireContext())
recycler.layoutManager = LinearLayoutManager(requireContext())
recycler.adapter = adapter
linear.addView(recycler)
val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
itemTouchHelper.attachToRecyclerView(recycler)
builder.setView(linear)
builder.setPositiveButton(
getString(android.R.string.ok)
) { _: DialogInterface?, _: Int ->
onChange(adapter.items)
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
dialog.show()
}
}

View file

@ -1,6 +1,5 @@
package com.deniscerri.ytdl.util
import android.R.color
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Dialog
@ -37,9 +36,11 @@ import androidx.lifecycle.withStarted
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.observeSources.ObserveSourcesItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory
import com.deniscerri.ytdl.util.Extensions.toTimePeriodsArray
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.tabs.TabLayout
@ -56,6 +57,7 @@ import java.util.Calendar
import java.util.Locale
import java.util.regex.Pattern
import kotlin.math.abs
import kotlin.math.min
object Extensions {
@ -289,19 +291,66 @@ object Extensions {
}
}
fun String.convertToTimestamp() : Int {
enum class Period {
HOUR, MINUTE, SECOND, MILLISECOND
}
fun Long.toTimePeriodsArray() : Map<Period, Int> {
var tmp = this
val millis = ((tmp % 1000) / 100).toInt()
tmp /= 1000
val hours = (tmp / 3600).toInt()
tmp %= 3600
val minutes = (tmp / 60).toInt()
tmp %= 60
val seconds = tmp.toInt()
return mapOf(
Period.HOUR to hours,
Period.MINUTE to minutes,
Period.SECOND to seconds,
Period.MILLISECOND to millis
)
}
fun Long.toStringTimeStamp() : String {
var tmp = this
val millis = ((tmp % 1000) / 100).toInt()
tmp /= 1000
val hours = (tmp / 3600).toInt()
tmp %= 3600
val minutes = (tmp / 60).toInt()
tmp %= 60
val seconds = tmp.toInt()
var res = "${minutes.toString().padStart(if (hours > 0) 2 else 1, '0')}:${seconds.toString().padStart(2, '0')}"
if (hours > 0){
res = "${hours}:" + res
}
if (millis > 0){
res += ".${millis}"
}
return res
}
fun String.convertToTimestamp() : Long {
return try {
val timeArray = this.split(":")
var timeSeconds = timeArray[timeArray.lastIndex].toInt()
val secondsMillis = timeArray[timeArray.lastIndex]
var timeSeconds = secondsMillis.split(".")[0].toLong()
val millis = kotlin.runCatching { secondsMillis.split(".")[1].toInt() }.getOrElse { 0 }
var times = 60
for (i in timeArray.lastIndex - 1 downTo 0) {
timeSeconds += timeArray[i].toInt() * times
times *= 60
}
timeSeconds
(timeSeconds * 1000) + millis * 100
}catch (e: Exception){
e.printStackTrace()
0
0L
}
}
@ -408,11 +457,15 @@ object Extensions {
paint.setColorFilter(PorterDuffColorFilter(colorValue.data, PorterDuff.Mode.SRC_IN))
val canvas = Canvas(bitmap)
DrawableCompat.setTint(drawable, colorValue.data)
DrawableCompat.setTint(drawable, ContextCompat.getColor(context, R.color.icon_fg))
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
fun DownloadItem.setAsScheduling(timeInMillis: Long) {
status = DownloadRepository.Status.Scheduled.toString()
downloadStartTime = timeInMillis
}
}

View file

@ -10,6 +10,8 @@ import android.util.Log
import android.webkit.MimeTypeMap
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import androidx.work.impl.utils.PreferenceUtils
import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.copyFolderTo
@ -294,7 +296,8 @@ object FileUtil {
}
fun getDownloadArchivePath(context: Context) : String {
return context.filesDir.absolutePath + "/download_archive.txt"
val folder = PreferenceManager.getDefaultSharedPreferences(context).getString("download_archive_path", context.filesDir.absolutePath + "/")!!
return "${formatPath(folder)}download_archive.txt"
}
fun getDefaultTerminalPath() : String {

View file

@ -21,6 +21,7 @@ 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.toStringDuration
import com.deniscerri.ytdl.work.TerminalDownloadWorker
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL
@ -488,6 +489,7 @@ class InfoUtil(private val context: Context) {
if (proxy!!.isNotBlank()) {
request.addOption("--proxy", proxy)
}
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
@ -546,7 +548,7 @@ class InfoUtil(private val context: Context) {
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
YoutubeDL.getInstance().execute(request){ progress, _, line ->
@ -631,6 +633,7 @@ class InfoUtil(private val context: Context) {
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
@ -904,6 +907,7 @@ class InfoUtil(private val context: Context) {
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
@ -1055,12 +1059,12 @@ class InfoUtil(private val context: Context) {
}
if(downloadItem.title.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.replace("\"", "\\\"").take(150)))
request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.take(150)))
}
if (downloadItem.author.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "video:uploader", ".+", downloadItem.author.replace("\"", "\\\"").take(30)))
request.addCommands(listOf("--replace-in-metadata", "video:uploader", ".+", downloadItem.author.take(30)))
}
request.addOption("--parse-metadata", "uploader:(?P<uploader>.+)(?: - Topic)$")
@ -1230,6 +1234,7 @@ class InfoUtil(private val context: Context) {
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
if (embedThumb) {
request.addOption("--embed-thumbnail")
if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!)
}
}
}
@ -1402,7 +1407,7 @@ class InfoUtil(private val context: Context) {
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters")
request.addOption("-o", "chapter:%(section_title)s.%(ext)s")
request.addOption("-o", "chapter:%(section_number)d - %(section_title)s.%(ext)s")
}else{
if (filenameTemplate.isNotBlank()){
request.addOption("-o", "${filenameTemplate.removeSuffix(".%(ext)s")}.%(ext)s")
@ -1494,7 +1499,6 @@ class InfoUtil(private val context: Context) {
}
}
audioFormats.forEachIndexed { idx, it -> formats.add(Format(audioFormatsValues[idx], containerPreference!!,"",acodecPreference!!, "",0, it)) }
audioFormats.reverse()
audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",resources.getString(R.string.preferred_format_id), "",1, it)) }
return formats
}
@ -1520,7 +1524,7 @@ class InfoUtil(private val context: Context) {
videoCodecs[videoCodecsValues.indexOf(this)]
}
}
videoFormats.reversed().forEach { formats.add(Format(it, containerPreference!!,videoCodecPreference,audioCodecPreference, "",0, it.split("_")[0])) }
videoFormats.forEach { formats.add(Format(it, containerPreference!!,videoCodecPreference,audioCodecPreference, "",0, it.split("_")[0])) }
formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.preferred_format_id),"", "",1, it)) }
return formats
}

View file

@ -15,6 +15,7 @@ import android.os.Build
import android.os.Bundle
import androidx.core.app.NotificationCompat
import androidx.core.content.FileProvider
import androidx.core.os.bundleOf
import androidx.documentfile.provider.DocumentFile
import androidx.navigation.NavDeepLinkBuilder
import com.deniscerri.ytdl.R
@ -33,6 +34,7 @@ class NotificationUtil(var context: Context) {
private val commandDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID)
private val finishedDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_FINISHED_CHANNEL_ID)
private val erroredDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_ERRORED_CHANNEL_ID)
private val miscDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_MISC_CHANNEL_ID)
private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java)
private val resources: Resources = context.resources
@ -99,6 +101,7 @@ class NotificationUtil(var context: Context) {
DOWNLOAD_FINISHED_CHANNEL_ID -> { return finishedDownloadNotificationBuilder }
DOWNLOAD_WORKER_CHANNEL_ID -> { return workerNotificationBuilder }
DOWNLOAD_ERRORED_CHANNEL_ID -> { return erroredDownloadNotificationBuilder }
DOWNLOAD_MISC_CHANNEL_ID -> { return miscDownloadNotificationBuilder }
}
return downloadNotificationBuilder
}
@ -336,13 +339,21 @@ class NotificationUtil(var context: Context) {
.setArguments(bundle)
.createPendingIntent()
val tabBundle = Bundle()
tabBundle.putString("tab", "error")
val errorTabPendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.downloadQueueMainFragment)
.setArguments(tabBundle)
.setArguments(bundleOf(Pair("tab", "error")))
.createPendingIntent()
val reconfigurePendingItent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.downloadQueueMainFragment)
.setArguments(
bundleOf(
Pair("tab", "error"),
Pair("reconfigure", id)
)
)
.createPendingIntent()
notificationBuilder
@ -359,6 +370,8 @@ class NotificationUtil(var context: Context) {
.setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions()
notificationBuilder.addAction(0, res.getString(R.string.configure_download), reconfigurePendingItent)
if (logID != null){
notificationBuilder.addAction(0, res.getString(R.string.logs), errorPendingIntent)
}
@ -618,6 +631,25 @@ class NotificationUtil(var context: Context) {
notificationManager.notify(QUERY_PROCESS_FINISHED_NOTIFICATION_ID, notificationBuilder.build())
}
fun createProcessingDownloads() : Notification{
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
notificationBuilder
.setContentTitle(resources.getString(R.string.processing))
.setSmallIcon(R.drawable.ic_app_icon)
.setLargeIcon(
BitmapFactory.decodeResource(
resources,
R.drawable.ic_app_icon
)
)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions()
return notificationBuilder.build()
}
companion object {
const val DOWNLOAD_SERVICE_CHANNEL_ID = "1"
const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2"

View file

@ -617,7 +617,7 @@ object UiUtil {
val command = bottomSheet.findViewById<Chip>(R.id.command)
when(status){
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused -> {
DownloadRepository.Status.Scheduled -> {
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) time!!.visibility = View.GONE
else {
val calendar = Calendar.getInstance()
@ -712,7 +712,8 @@ object UiUtil {
download!!.visibility = View.GONE
}
DownloadRepository.Status.Scheduled -> {
download!!.text = context.getString(R.string.download_now)
download!!.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_downloads, 0, 0, 0);
download.text = context.getString(R.string.download_now)
}
else -> {
download?.setOnLongClickListener {
@ -813,6 +814,7 @@ object UiUtil {
val codec = bottomSheet.findViewById<TextView>(R.id.codec)
val fileSize = bottomSheet.findViewById<TextView>(R.id.file_size)
val command = bottomSheet.findViewById<Chip>(R.id.command)
val location = bottomSheet.findViewById<Chip>(R.id.location)
val file = File(item.downloadPath.first())
val calendar = Calendar.getInstance()
@ -858,6 +860,11 @@ object UiUtil {
showGeneratedCommand(context, item.command)
}
location?.isVisible = true
location?.setOnClickListener {
showFullTextDialog(context, item.downloadPath.joinToString("\n"), context.getString(R.string.location))
}
val link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link)
val url = item.url
@ -1664,169 +1671,6 @@ object UiUtil {
errDialog.show()
}
@SuppressLint("SetTextI18n")
fun handleExistingDownloadsResponse(context: Activity, lifecycleScope: CoroutineScope, supportFragmentManager: FragmentManager, it: DownloadViewModel.AlreadyExistsUIState, downloadViewModel: DownloadViewModel, historyViewModel: HistoryViewModel){
if (it.downloadItems.isEmpty() && it.historyItems.isEmpty()) return
val title = context.getString(R.string.download_already_exists)
val message = context.getString(R.string.download_already_exists_summary)
val sheet = BottomSheetDialog(context)
sheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
sheet.setContentView(R.layout.download_multiple_bottom_sheet)
sheet.findViewById<TextView>(R.id.bottom_sheet_title)?.apply {
text = title
textSize = 20f
}
sheet.findViewById<TextView>(R.id.bottom_sheet_subtitle)?.text = message
sheet.findViewById<LinearLayout>(R.id.multiple_top_info)?.isVisible = false
var adapter: AlreadyExistsAdapter? = null
val list = mutableListOf<Pair<DownloadItem, Long?>>()
val alreadyExistsClickListener = object: AlreadyExistsAdapter.OnItemClickListener {
override fun onEditItem(downloadItem: DownloadItem, position: Int) {
val resultItem = downloadViewModel.createResultItemFromDownload(downloadItem)
val onItemUpdated = object: ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
override fun onDownloadItemUpdate(
resultItemID: Long,
item: DownloadItem
) {
val currentIndex = list.indexOfFirst { it.first.id == item.id }
val current = list[currentIndex]
list[currentIndex] = Pair(item, current.second)
adapter?.submitList(list)
adapter?.notifyItemChanged(position)
}
}
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem, onItemUpdated)
bottomSheet.show(supportFragmentManager, "configureDownloadSingleSheet")
}
override fun onDeleteItem(downloadItem: DownloadItem, position: Int, historyID: Long?) {
showGenericDeleteDialog(context, downloadItem.title){
if (historyID == null){
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteDownload(downloadItem.id)
}
}
list.removeAll { it.first.id == downloadItem.id }
if (list.isEmpty()){
sheet.dismiss()
}
adapter?.notifyDataSetChanged()
}
}
override fun onShowHistoryItem(id: Long) {
lifecycleScope.launch {
val historyItem = withContext(Dispatchers.IO){
downloadViewModel.getHistoryItemById(id)
}
showHistoryItemDetailsCard(historyItem, context, isPresent = true,
removeItem = { item, deleteFile ->
historyViewModel.delete(item, deleteFile)
},
redownloadItem = { },
redownloadShowDownloadCard = {}
)
}
}
}
adapter = AlreadyExistsAdapter(alreadyExistsClickListener, context)
sheet.findViewById<RecyclerView>(R.id.downloadMultipleRecyclerview)?.apply {
layoutManager = GridLayoutManager(context, 1)
this.adapter = adapter
enableFastScroll()
setPadding(0,0,0,0)
}
CoroutineScope(Dispatchers.IO).launch {
it.downloadItems.forEach {
val downloadItem = downloadViewModel.getItemByID(it)
list.add(Pair(downloadItem, null))
adapter.submitList(list)
}
it.historyItems.forEach {
val historyItem = downloadViewModel.getHistoryItemById(it) ?: return@forEach
val downloadItem = downloadViewModel.createDownloadItemFromHistory(historyItem)
downloadItem.id = it
list.add(Pair(downloadItem, historyItem.id))
adapter.submitList(list)
}
}
sheet.findViewById<Button>(R.id.bottomsheet_download_button)?.apply {
setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteProcessing()
val items = list.map { it.first }
items.forEach { it.id = 0 }
downloadViewModel.queueDownloads(items, true)
withContext(Dispatchers.Main){
sheet.dismiss()
}
}
}
}
sheet.findViewById<Button>(R.id.bottomsheet_schedule_button)?.apply {
setOnClickListener {
showDatePicker(supportFragmentManager) { calendar ->
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteProcessing()
val items = list.map { it.first }
items.forEach {
it.id = 0
it.downloadStartTime = calendar.timeInMillis
}
runBlocking {
downloadViewModel.queueDownloads(items, true)
val first = items.first()
val date = SimpleDateFormat(
DateFormat.getBestDateTimePattern(
Locale.getDefault(),
"ddMMMyyyy - HHmm"
), Locale.getDefault()
).format(first.downloadStartTime)
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.download_rescheduled_to) + " " + date,
Toast.LENGTH_LONG
).show()
}
}
withContext(Dispatchers.Main) {
sheet.dismiss()
}
}
}
}
}
sheet.setOnDismissListener { _ ->
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteProcessing()
downloadViewModel.deleteAllWithID(it.downloadItems)
}
}
sheet.findViewById<BottomAppBar>(R.id.bottomAppBar)?.isVisible = false
sheet.show()
}
fun showGenericDeleteDialog(context: Context, itemTitle: String, accepted: () -> Unit){
val deleteDialog = MaterialAlertDialogBuilder(context)
deleteDialog.setTitle(context.getString(R.string.you_are_going_to_delete) + " \"" + itemTitle + "\"!")

View file

@ -1,18 +1,28 @@
package com.deniscerri.ytdl.work
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.receiver.CancelScheduleAlarmReceiver
import com.deniscerri.ytdl.receiver.ScheduleAlarmReceiver
import java.util.Calendar
import java.util.concurrent.TimeUnit
class AlarmScheduler(private val context: Context) {
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
@SuppressLint("ScheduleExactAlarm")
fun schedule() {
cancel()
@ -25,17 +35,15 @@ class AlarmScheduler(private val context: Context) {
val time = calculateNextTime(sTime)
//schedule starting work
val workConstraints = Constraints.Builder()
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
.addTag("scheduledDownload")
.addTag("download")
.setConstraints(workConstraints.build())
.setInitialDelay(System.currentTimeMillis() - time.timeInMillis, TimeUnit.MILLISECONDS)
WorkManager.getInstance(context).enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
alarmManager?.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
time.timeInMillis,
PendingIntent.getBroadcast(
context,
0,
Intent(context, ScheduleAlarmReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
//schedule closing work
@ -46,27 +54,35 @@ class AlarmScheduler(private val context: Context) {
sTime.set(Calendar.SECOND, 0)
val calendar = calculateNextTime(eTime)
val workRequest2 = OneTimeWorkRequestBuilder<CancelScheduledDownloadWorker>()
.addTag("cancelScheduledDownload")
.setConstraints(workConstraints.build())
.setInitialDelay(System.currentTimeMillis() - calendar.timeInMillis + 60000, TimeUnit.MILLISECONDS)
WorkManager.getInstance(context).enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest2.build()
alarmManager?.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
PendingIntent.getBroadcast(
context,
0,
Intent(context, CancelScheduleAlarmReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
}
fun cancel() {
val wm = WorkManager.getInstance(context)
wm.cancelAllWorkByTag("scheduledDownload")
wm.cancelAllWorkByTag("cancelScheduledDownload")
val intent = Intent(context, ScheduleAlarmReceiver::class.java)
val pendingIntent = PendingIntent.getService(context, 0, intent,
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE)
val cancelIntent = Intent(context, CancelScheduleAlarmReceiver::class.java)
val cancelPendingIntent = PendingIntent.getService(context, 0, cancelIntent,
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE)
kotlin.runCatching {
alarmManager?.cancel(pendingIntent)
alarmManager?.cancel(cancelPendingIntent)
}
}
fun calculateNextTime(c: Calendar) : Calendar {
private fun calculateNextTime(c: Calendar) : Calendar {
val calendar = Calendar.getInstance()
if (c.get(Calendar.HOUR_OF_DAY) < calendar.get(Calendar.HOUR_OF_DAY)){
c.add(Calendar.DATE, 1)
@ -110,4 +126,12 @@ class AlarmScheduler(private val context: Context) {
return false
}
fun canSchedule() : Boolean {
return if (Build.VERSION.SDK_INT >= 31){
alarmManager?.canScheduleExactAlarms() == true
}else {
false
}
}
}

View file

@ -58,15 +58,9 @@ class CancelScheduledDownloadWorker(
WorkManager.getInstance(context).cancelAllWorkByTag("download")
runningDownloads.forEach {
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
it.status = DownloadRepository.Status.Queued.toString()
dao.update(it)
}
return Result.success()
}
companion object {
val runningYTDLInstances: MutableList<Long> = mutableListOf()
const val TAG = "DownloadWorker"
}
}

View file

@ -119,7 +119,10 @@ class DownloadWorker(
delay(1500)
//update item if its incomplete
resultRepo.updateDownloadItem(downloadItem)?.apply {
dao.updateWithoutUpsert(this)
val status = dao.checkStatus(this.id)
if (listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused).contains(status)){
dao.updateWithoutUpsert(this)
}
}
}
@ -172,9 +175,7 @@ class DownloadWorker(
}
}
}.onSuccess {
resultRepo.updateDownloadItem(downloadItem)?.apply {
dao.updateWithoutUpsert(this)
}
resultRepo.updateDownloadItem(downloadItem)
val wasQuickDownloaded = resultDao.getCountInt() == 0
runBlocking {
var finalPaths : MutableList<String>?
@ -241,9 +242,9 @@ class DownloadWorker(
val incognito = sharedPreferences.getBoolean("incognito", false)
if (!incognito) {
if (request.hasOption("--download-archive") && finalPaths == listOf(context.getString(R.string.unfound_file))) {
Looper.prepare().run {
handler.postDelayed({
Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
}
}, 100)
}else{
val unixTime = System.currentTimeMillis() / 1000
finalPaths?.apply {
@ -303,6 +304,8 @@ class DownloadWorker(
}
}.onFailure {
if (this@DownloadWorker.isStopped) return@onFailure
FileUtil.deleteConfigFiles(request)
withContext(Dispatchers.Main){
@ -345,7 +348,6 @@ class DownloadWorker(
setProgressAsync(workDataOf("progress" to 100, "output" to it.toString(), "id" to downloadItem.id))
}
//if (items.size <= 1) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
}
}
}

View file

@ -7,6 +7,7 @@ import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
@ -205,7 +206,14 @@ class ObserveSourceWorker(
}
//schedule for next time
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
.addTag("observeSources")
.addTag(sourceID.toString())

View file

@ -223,55 +223,49 @@
</com.facebook.shimmer.ShimmerFrameLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<LinearLayout
android:id="@+id/home_fabs"
android:layout_width="match_parent"
android:layout_height="match_parent" >
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="bottom|end">
<LinearLayout
android:layout_width="match_parent"
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="10dp"
android:layout_gravity="bottom|end">
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_all_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="10dp"
android:layout_marginHorizontal="16dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_all_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_selected_fab"
android:visibility="gone"
app:elevation="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:layout_marginBottom="10dp"
app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_selected_fab"
android:visibility="gone"
app:elevation="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -234,55 +234,49 @@
</com.facebook.shimmer.ShimmerFrameLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<LinearLayout
android:id="@+id/home_fabs"
android:layout_width="match_parent"
android:layout_height="match_parent" >
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="bottom|end">
<LinearLayout
android:layout_width="match_parent"
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="10dp"
android:layout_gravity="bottom|end">
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_all_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="10dp"
android:layout_marginHorizontal="16dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_all_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_selected_fab"
android:visibility="gone"
app:elevation="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:layout_marginBottom="10dp"
app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_selected_fab"
android:visibility="gone"
app:elevation="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -223,55 +223,49 @@
</com.facebook.shimmer.ShimmerFrameLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<LinearLayout
android:id="@+id/home_fabs"
android:layout_width="match_parent"
android:layout_height="match_parent" >
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="bottom|end">
<LinearLayout
android:layout_width="match_parent"
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="10dp"
android:layout_gravity="bottom|end">
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_all_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="10dp"
android:layout_marginHorizontal="16dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_all_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_selected_fab"
android:visibility="gone"
app:elevation="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:layout_marginBottom="10dp"
app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_selected_fab"
android:visibility="gone"
app:elevation="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -253,56 +253,50 @@
</com.facebook.shimmer.ShimmerFrameLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<LinearLayout
android:id="@+id/home_fabs"
android:layout_width="match_parent"
android:layout_height="match_parent" >
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="bottom|end">
<LinearLayout
android:layout_width="match_parent"
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="10dp"
android:layout_gravity="bottom|end">
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:text="@string/link_you_copied"
android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_all_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="10dp"
android:layout_marginHorizontal="16dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_all_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_selected_fab"
android:visibility="gone"
app:elevation="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:layout_marginBottom="10dp"
app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_selected_fab"
android:visibility="gone"
app:elevation="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:id="@+id/parent"
android:layout_height="wrap_content">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/linearLayout3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/drag_handle">
<LinearLayout
android:id="@+id/hours_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintCircle="@id/parent"
app:layout_constraintCircleRadius="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/hour" />
<NumberPicker
android:id="@+id/hours"
android:layout_width="wrap_content"
android:layout_height="100dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/minutes_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintCircle="@id/parent"
app:layout_constraintCircleRadius="0dp"
app:layout_constraintStart_toEndOf="@id/hours_container"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/minute" />
<NumberPicker
android:id="@+id/minutes"
android:layout_width="wrap_content"
android:layout_height="100dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/seconds_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintCircle="@id/parent"
app:layout_constraintCircleRadius="0dp"
app:layout_constraintStart_toEndOf="@id/minutes_container"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/second" />
<NumberPicker
android:id="@+id/seconds"
android:layout_width="wrap_content"
android:layout_height="100dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/milliseconds_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintCircle="@id/parent"
app:layout_constraintCircleRadius="0dp"
app:layout_constraintStart_toEndOf="@id/seconds_container"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/milliseconds" />
<NumberPicker
android:id="@+id/milliseconds"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_gravity="center_horizontal" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -76,14 +76,12 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="3"
app:layout_constraintVertical_bias="0.0"
android:maxLines="2"
android:paddingHorizontal="5dp"
android:scrollbars="none"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/horizontalScrollView"
app:layout_constraintEnd_toStartOf="@+id/already_exists_buttons"
app:layout_constraintEnd_toStartOf="@+id/options"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -92,10 +90,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="title,already_exists_buttons" />
app:constraint_referenced_ids="title,options" />
<HorizontalScrollView
android:id="@+id/horizontalScrollView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="bottom"
@ -112,6 +109,44 @@
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/download_type"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/format_note"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
@ -153,24 +188,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
@ -178,39 +196,25 @@
</HorizontalScrollView>
<LinearLayout
android:id="@+id/already_exists_buttons"
android:layout_width="wrap_content"
<com.google.android.material.button.MaterialButton
android:id="@+id/options"
style="?attr/materialIconButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="0dp"
android:clickable="true"
android:focusable="true"
android:padding="0dp"
app:cornerRadius="10dp"
app:icon="@drawable/baseline_more_vert_24"
app:iconTint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<Button
android:id="@+id/already_exists_edit"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_edit"
android:contentDescription="@string/edit"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="1.0" />
<Button
android:id="@+id/already_exists_delete"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:contentDescription="@string/Remove"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_delete_24"
app:layout_constraintEnd_toEndOf="parent" />
</LinearLayout>
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</com.google.android.material.card.MaterialCardView>

View file

@ -95,6 +95,23 @@
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
app:layout_constraintEnd_toEndOf="@+id/frame_layout" />
<com.google.android.material.button.MaterialButton
android:id="@+id/forward"
android:clickable="false"
app:iconSize="50dp"
style="@style/Widget.Material3.Button.IconButton"
app:iconTint="?android:colorAccent"
app:icon="@drawable/exomedia_ic_fast_forward_white"
android:layout_marginHorizontal="20dp"
android:layout_width="wrap_content"
android:contentDescription="@string/end"
android:indeterminate="true"
app:layout_constraintHorizontal_bias="0.0"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/durationText"
@ -162,6 +179,7 @@
android:hint="@string/start">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/from_textinput_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
@ -189,6 +207,7 @@
app:layout_constraintStart_toEndOf="@+id/colon">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/to_textinput_edittext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="text" />

View file

@ -124,12 +124,18 @@
</LinearLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loadingItemsProgress"
android:layout_width="match_parent"
android:indeterminate="true"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloadMultipleRecyclerview"
android:layout_width="match_parent"
android:paddingTop="10dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="70dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

View file

@ -17,91 +17,64 @@
android:id="@+id/scr"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:scrollbars="none"
android:paddingTop="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
<com.google.android.material.card.MaterialCardView
android:id="@+id/current"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_marginHorizontal="20dp"
android:layout_marginVertical="5dp"
app:layout_constraintTop_toBottomOf="@+id/title">
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:paddingTop="20dp">
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/bottom_sheet_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/use_extra_commands"
android:text="@string/current"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:textIsSelectable="true"
android:id="@+id/currentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/current"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginVertical="5dp"
app:layout_constraintTop_toBottomOf="@+id/title">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/current"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:textIsSelectable="true"
android:id="@+id/currentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</ScrollView>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="5dp"
android:contentDescription="@string/extra_command"
android:hint="@string/extra_command"
app:layout_constraintTop_toBottomOf="@+id/scr">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/command"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textSize="14sp"
android:inputType="textMultiLine"
android:maxLines="2000" />

View file

@ -13,6 +13,8 @@
android:id="@+id/file_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -37,7 +39,7 @@
android:scrollbars="none"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
@ -114,19 +116,6 @@
</HorizontalScrollView>
<com.google.android.material.button.MaterialButton
android:id="@+id/action_button"
style="?attr/materialIconButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="0dp"
android:padding="0dp"
app:cornerRadius="10dp"
app:iconTint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -18,7 +18,7 @@
android:scrollbars="none"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
android:paddingBottom="100dp"
android:layout_marginBottom="100dp"
app:layout_constraintTop_toBottomOf="@+id/pause_resume">
</androidx.recyclerview.widget.RecyclerView>

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:paddingTop="20dp">
<TextView
android:id="@+id/bottom_sheet_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/download_already_exists"
android:textSize="18sp"
android:maxLines="2"
android:layout_marginTop="5dp"
android:singleLine="false"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_download_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/bottom_sheet_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/download_already_exists_summary"
android:textSize="11sp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_download_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_title" />
<Button
android:id="@+id/bottomsheet_download_button"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/download"
app:icon="@drawable/ic_down"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloadMultipleRecyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="20dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -67,6 +67,7 @@
android:id="@+id/container_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textDirection="locale"
android:inputType="none"
app:simpleItems="@array/audio_containers"
/>

View file

@ -152,17 +152,12 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<LinearLayout
android:id="@+id/home_fabs"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="10dp"
android:layout_gravity="bottom|end">
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="bottom|end">
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab"
@ -176,31 +171,30 @@
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_all_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginTop="10dp"
android:layout_marginHorizontal="16dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_all_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="10dp"
android:layout_marginHorizontal="16dp"
android:text="@string/download_all"
app:icon="@drawable/ic_down" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_selected_fab"
android:visibility="gone"
app:elevation="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_selected_fab"
android:visibility="gone"
app:elevation="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:layout_marginBottom="10dp"
app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -155,6 +155,21 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/location"
android:visibility="gone"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:minWidth="30dp"
app:cornerRadius="10dp"
android:text="@string/location"
app:chipIcon="@drawable/baseline_folder_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>

View file

@ -93,6 +93,8 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/download_item_data"
android:layout_width="0dp"
android:clickable="false"
android:focusable="false"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -232,6 +234,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="0dp"
android:clickable="true"
android:focusable="true"
android:padding="0dp"
app:cornerRadius="10dp"
app:icon="@drawable/baseline_more_vert_24"

View file

@ -129,6 +129,13 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loadingItemsProgress"
android:layout_width="match_parent"
android:indeterminate="true"
android:layout_marginTop="10dp"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloadMultipleRecyclerview"
android:layout_width="match_parent"

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/drag_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:clickable="false"
android:focusable="false"
android:paddingTop="5dp"
android:paddingEnd="20dp"
android:paddingStart="0dp"
android:tintMode="src_in"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_drag_handle"
app:tint="?attr/colorControlNormal"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/textContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="17sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/drag_view"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/edit"
android:title="@string/edit" />
<item
android:id="@+id/delete"
android:title="@string/Remove" />
<item
android:id="@+id/copy_url"
android:icon="@drawable/ic_copy"
android:title="@string/copy_url" />
</menu>

View file

@ -0,0 +1,42 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity" >
<item
android:id="@+id/select_between"
android:visible="false"
android:title="@string/select_between"
android:icon="@drawable/ic_select_between"
app:showAsAction="ifRoom" />
<item
android:id="@+id/delete_results"
android:title="@string/remove_results"
android:icon="@drawable/baseline_delete_24"
app:showAsAction="ifRoom" />
<item
android:id="@+id/download"
android:title="@string/download"
android:icon="@drawable/baseline_download_24"
app:showAsAction="ifRoom" />
<item
android:id="@+id/select_all"
android:title="@string/select_all"
app:showAsAction="never" />
<item
android:id="@+id/invert_selected"
android:title="@string/invert_selected"
app:showAsAction="never" />
<item
android:id="@+id/copy_urls"
android:icon="@drawable/ic_delete_all"
android:title="@string/copy_urls"
app:showAsAction="never" />
</menu>

View file

@ -33,6 +33,9 @@
<action
android:id="@+id/action_homeFragment_to_downloadMultipleBottomSheetDialog2"
app:destination="@id/downloadMultipleBottomSheetDialog2" />
<action
android:id="@+id/action_homeFragment_to_downloadsAlreadyExistDialog"
app:destination="@id/downloadsAlreadyExistDialog" />
</fragment>
<fragment
android:id="@+id/historyFragment"
@ -42,6 +45,9 @@
<action
android:id="@+id/action_historyFragment_to_downloadQueueMainFragment"
app:destination="@id/downloadQueueMainFragment" />
<action
android:id="@+id/action_historyFragment_to_downloadsAlreadyExistDialog"
app:destination="@id/downloadsAlreadyExistDialog" />
</fragment>
<fragment
android:id="@+id/moreFragment"
@ -191,4 +197,8 @@
android:id="@+id/observeSourcesBottomSheetDialog"
android:name="com.deniscerri.ytdl.ui.downloadcard.ObserveSourcesBottomSheetDialog"
android:label="ObserveSourcesBottomSheetDialog" />
<dialog
android:id="@+id/downloadsAlreadyExistDialog"
android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadsAlreadyExistDialog"
android:label="DownloadsAlreadyExistDialog" />
</navigation>

View file

@ -34,4 +34,8 @@
android:id="@+id/downloadMultipleBottomSheetDialog"
android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadMultipleBottomSheetDialog"
android:label="DownloadMultipleBottomSheetDialog" />
<dialog
android:id="@+id/downloadsAlreadyExistDialog2"
android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadsAlreadyExistDialog"
android:label="DownloadsAlreadyExistDialog" />
</navigation>

View file

@ -40,37 +40,37 @@
</string-array>
<string-array name="video_formats">
<item>@string/worst_quality</item>
<item>240p</item>
<item>360p</item>
<item>480p</item>
<item>720p</item>
<item>1080p</item>
<item>1440p</item>
<item>2160p</item>
<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>@string/worst_quality</item>
</string-array>
<string-array name="video_formats_values">
<item>worst</item>
<item>240p_ytdlnisgeneric</item>
<item>360p_ytdlnisgeneric</item>
<item>480p_ytdlnisgeneric</item>
<item>720p_ytdlnisgeneric</item>
<item>1080p_ytdlnisgeneric</item>
<item>1440p_ytdlnisgeneric</item>
<item>2160p_ytdlnisgeneric</item>
<item>best</item>
<item>2160p_ytdlnisgeneric</item>
<item>1440p_ytdlnisgeneric</item>
<item>1080p_ytdlnisgeneric</item>
<item>720p_ytdlnisgeneric</item>
<item>480p_ytdlnisgeneric</item>
<item>360p_ytdlnisgeneric</item>
<item>240p_ytdlnisgeneric</item>
<item>worst</item>
</string-array>
<string-array name="audio_formats">
<item>worst</item>
<item>best</item>
<item>worst</item>
</string-array>
<string-array name="audio_formats_values">
<item>wa</item>
<item>ba</item>
<item>wa</item>
</string-array>
<string-array name="format_ordering">
@ -896,6 +896,7 @@
<item>%(uploader)s___(string): Full name of the video uploader)</item>
<item>%(title)s___(string): Video title)</item>
<item>%(id)s___(string): Video identifier)</item>
<item>%(playlist_index,playlist_autonumber&amp;{}. |)s%(title)s___(string): Title with playlist index in front if its available</item>
<item>%(fulltitle)s___(string): Video title ignoring live timestamp and generic title)</item>
<!-- <item>%(ext)s___(string): Video filename extension)</item>-->
<item>%(alt_title)s___(string): A secondary title of the video)</item>
@ -1418,4 +1419,34 @@
<item>config</item>
</string-array>
<string-array name="format_importance_audio">
<item>@string/preferred_format_id</item>
<item>@string/language</item>
<item>@string/codec</item>
<item>@string/container</item>
</string-array>
<string-array name="format_importance_audio_values">
<item>id</item>
<item>language</item>
<item>codec</item>
<item>container</item>
</string-array>
<string-array name="format_importance_video">
<item>@string/preferred_format_id</item>
<item>@string/video_quality</item>
<item>@string/codec</item>
<item>@string/no_audio</item>
<item>@string/container</item>
</string-array>
<string-array name="format_importance_video_values">
<item>id</item>
<item>resolution</item>
<item>codec</item>
<item>no_audio</item>
<item>container</item>
</string-array>
</resources>

View file

@ -391,4 +391,15 @@
<string name="move_bottom">Move to the Bottom</string>
<string name="scheduled">Scheduled</string>
<string name="clear_scheduled">Clear Scheduled</string>
<string name="minute">Minute</string>
<string name="second">Second</string>
<string name="milliseconds">Milliseconds</string>
<string name="format_importance_audio">Format Importance Order (Audio)</string>
<string name="format_importance_video">Format Importance Order (Video)</string>
<string name="format_importance_note">Adjust format element importance. This order will only be used when the app fetches the formats in the download card and auto-selects the format!</string>
<string name="no_audio">Video with no Audio</string>
<string name="download_archive_folder">Download Archive Folder</string>
<string name="location">Location</string>
<string name="enable_alarm_permission">You need to enable the SCHEDULE_EXACT_ALARM permission in the app settings.</string>
<string name="process_downloads_background">Downloads are still loading. Continue procesing them in the background and start downloading?</string>
</resources>

View file

@ -37,6 +37,12 @@
app:useSimpleSummaryProvider="true"
app:title="@string/prevent_duplicate_downloads" />
<Preference
android:defaultValue=""
app:icon="@drawable/baseline_archive_24"
app:key="download_archive_path"
app:title="@string/download_archive_folder" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"

View file

@ -71,7 +71,7 @@
<PreferenceCategory android:title="@string/misc">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:defaultValue="true"
android:icon="@drawable/baseline_recommend_24"
android:key="home_recommendations"
app:summary="@string/video_recommendations_summary"
@ -145,7 +145,7 @@
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:defaultValue="true"
android:icon="@drawable/baseline_numbers_24"
android:key="show_count_downloads"
app:title="@string/show_download_count" />

View file

@ -54,6 +54,14 @@
android:summary="@string/use_extra_commands_summary"
app:title="@string/use_extra_commands" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_cut"
app:key="force_keyframes"
app:summary="@string/force_keyframes_summary"
app:title="@string/force_keyframes" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/audio">
@ -82,14 +90,6 @@
app:summary="@string/crop_thumb_summary"
app:title="@string/crop_thumb" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_cut"
app:key="force_keyframes"
app:summary="@string/force_keyframes_summary"
app:title="@string/force_keyframes" />
</PreferenceCategory>
@ -144,7 +144,7 @@
app:title="@string/save_thumb" />
<ListPreference
android:defaultValue="png"
android:defaultValue="jpg"
android:entries="@array/thumbnail_containers"
android:entryValues="@array/thumbnail_containers_values"
android:icon="@drawable/ic_image"
@ -254,6 +254,17 @@
app:icon="@drawable/ic_format"
app:key="prefer_smaller_formats"
app:title="@string/prefer_smaller_formats" />
<Preference
android:icon="@drawable/ic_format"
android:key="format_importance_audio"
app:title="@string/format_importance_audio" />
<Preference
android:icon="@drawable/ic_format"
android:key="format_importance_video"
app:title="@string/format_importance_video" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -81,7 +81,7 @@
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:defaultValue="true"
android:icon="@drawable/ic_update_app"
android:key="update_app"
app:summary="@string/update_app_summary"

View file

@ -14,7 +14,7 @@ buildscript {
commonsIoVer = '2.5'
// supports java 1.6
commonsCompressVer = '1.12'
youtubedlAndroidVer = "-SNAPSHOT"
youtubedlAndroidVer = "ed652962e5"
youtubedlAndroidVerNoPyCrypto = "97bee6c0e8"
oldytdlAndroidVer = "0.14.0"
workVer = "2.9.0"
@ -37,12 +37,12 @@ buildscript {
}
plugins {
id 'com.android.application' version '8.3.1' apply false
id 'com.android.library' version '8.3.1' apply false
id 'com.android.application' version '8.4.0' apply false
id 'com.android.library' version '8.4.0' apply false
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
id "org.jetbrains.kotlin.plugin.serialization" version "1.8.10" apply false
id "org.jetbrains.kotlin.plugin.parcelize" version "1.8.10" apply false
id 'com.android.test' version '8.3.1' apply false
id 'com.android.test' version '8.4.0' apply false
id 'com.google.devtools.ksp' version '1.8.10-1.0.9' apply false
}

View file

@ -1,6 +1,6 @@
#Wed Oct 18 18:36:39 CEST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists