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 properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 7 def versionMinor = 7
def versionPatch = 5 def versionPatch = 6
def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def versionExt = "" def isBeta = true
def versionExt = isBeta ? ".${versionBuild}-beta" : ""
if (versionBuild > 0){
versionExt = ".${versionBuild}-beta"
}
android { android {
namespace 'com.deniscerri.ytdl' 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.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- for display over apps-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <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 <application
android:name=".App" android:name=".App"
@ -127,6 +134,24 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <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:scheme="https" />
<data android:host="instagram.com" /> <data android:host="instagram.com" />
</intent-filter> </intent-filter>
@ -136,6 +161,15 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <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:scheme="https" />
<data android:host="tiktok.com" /> <data android:host="tiktok.com" />
</intent-filter> </intent-filter>
@ -181,6 +215,33 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <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:scheme="https" />
<data android:host="pinterest.com" /> <data android:host="pinterest.com" />
</intent-filter> </intent-filter>
@ -356,6 +417,8 @@
<receiver android:name=".receiver.CancelDownloadNotificationReceiver" /> <receiver android:name=".receiver.CancelDownloadNotificationReceiver" />
<receiver android:name=".receiver.CancelWorkReceiver" /> <receiver android:name=".receiver.CancelWorkReceiver" />
<receiver android:name=".receiver.PauseDownloadNotificationReceiver" /> <receiver android:name=".receiver.PauseDownloadNotificationReceiver" />
<receiver android:name=".receiver.CancelScheduleAlarmReceiver" />
<receiver android:name=".receiver.ScheduleAlarmReceiver" />
<activity <activity
android:name=".receiver.ResumeActivity" android:name=".receiver.ResumeActivity"
@ -370,6 +433,8 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name=".services.ProcessDownloadsInBackgroundService" android:exported="false" />
<service <service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false" 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.DownloadItem
import com.deniscerri.ytdl.database.models.DownloadItemSimple import com.deniscerri.ytdl.database.models.DownloadItemSimple
import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.repository.DownloadRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@ -130,7 +131,7 @@ interface DownloadDao {
fun getLastDownloadId(): Long fun getLastDownloadId(): Long
@Query("SELECT status FROM downloads WHERE id=:id") @Query("SELECT status FROM downloads WHERE id=:id")
fun checkStatus(id: Long) : String fun checkStatus(id: Long) : DownloadRepository.Status?
@Query("UPDATE downloads " + @Query("UPDATE downloads " +
"SET status = CASE " + "SET status = CASE " +
@ -178,7 +179,7 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE id in (:list)") @Query("DELETE FROM downloads WHERE id in (:list)")
suspend fun deleteAllWithIDs(list: List<Long>) suspend fun deleteAllWithIDs(list: List<Long>)
@Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','QueuedPaused','Active','ActivePaused')") @Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','QueuedPaused','Active','ActivePaused', 'Scheduled')")
suspend fun cancelActiveQueued() suspend fun cancelActiveQueued()
@Query("DELETE FROM downloads WHERE status='Processing' AND id=:id") @Query("DELETE FROM downloads WHERE status='Processing' AND id=:id")
@ -229,9 +230,12 @@ interface DownloadDao {
@Query("Select url from downloads where id in (:ids)") @Query("Select url from downloads where id in (:ids)")
fun getURLsByID(ids: List<Long>) : List<String> 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>) suspend fun resetScheduleTimeForItems(list: List<Long>)
@Query("UPDATE downloads SET status='Queued' where status in ('QueuedPaused', 'ActivePaused')")
suspend fun resetPausedItems()
@Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)") @Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)")
suspend fun reQueueDownloadItems(list: List<Long>) suspend fun reQueueDownloadItems(list: List<Long>)

View file

@ -37,6 +37,9 @@ interface ResultDao {
return insertMultiple(items.filter { getResultByURL(it.url) == null }) 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) @Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(item: ResultItem) 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() val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED) if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>() val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download") .addTag("download")

View file

@ -129,6 +129,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return resultDao.getResultByURL(url) 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> { suspend fun getResultsFromSource(inputQuery: String, resetResults: Boolean, addToResults: Boolean = true, singleItem: Boolean = false) : ArrayList<ResultItem> {
return when(getQueryType(inputQuery)){ return when(getQueryType(inputQuery)){
SourceType.YOUTUBE_VIDEO -> { 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 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) val m = p.matcher(inputQuery)
if (m.find()) { if (m.find()) {
type = SourceType.YOUTUBE_VIDEO type = SourceType.YOUTUBE_VIDEO

View file

@ -5,8 +5,11 @@ import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log import android.widget.Toast
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData 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.AlarmScheduler
import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow 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.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
@ -56,6 +63,7 @@ import java.util.Locale
class DownloadViewModel(private val application: Application) : AndroidViewModel(application) { class DownloadViewModel(private val application: Application) : AndroidViewModel(application) {
private val dbManager: DBManager private val dbManager: DBManager
val repository : DownloadRepository val repository : DownloadRepository
private val sharedDownloadViewModel: SharedDownloadViewModel
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao private val commandTemplateDao: CommandTemplateDao
private val infoUtil : InfoUtil private val infoUtil : InfoUtil
@ -78,6 +86,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val scheduledDownloadsCount : Flow<Int> val scheduledDownloadsCount : Flow<Int>
val pausedDownloadsCount: Flow<Int> val pausedDownloadsCount: Flow<Int>
val alreadyExistsUiState: MutableStateFlow<List<SharedDownloadViewModel.AlreadyExistsIDs>>
private var bestVideoFormat : Format private var bestVideoFormat : Format
private var bestAudioFormat : Format private var bestAudioFormat : Format
private var defaultVideoFormats : MutableList<Format> private var defaultVideoFormats : MutableList<Format>
@ -97,16 +107,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
private val historyRepository: HistoryRepository private val historyRepository: HistoryRepository
private val resultRepository: ResultRepository 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 { enum class Type {
auto, audio, video, command auto, audio, video, command
} }
@ -121,6 +121,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dbManager = DBManager.getInstance(application) dbManager = DBManager.getInstance(application)
dao = dbManager.downloadDao dao = dbManager.downloadDao
repository = DownloadRepository(dao) repository = DownloadRepository(dao)
sharedDownloadViewModel = SharedDownloadViewModel(application)
alreadyExistsUiState = sharedDownloadViewModel.alreadyExistsUiState
historyRepository = HistoryRepository(dbManager.historyDao) historyRepository = HistoryRepository(dbManager.historyDao)
resultRepository = ResultRepository(dbManager.resultDao, application) resultRepository = ResultRepository(dbManager.resultDao, application)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
@ -168,7 +170,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
audioContainer = sharedPreferences.getString("audio_format", "mp3") audioContainer = sharedPreferences.getString("audio_format", "mp3")
bestAudioFormat = if (audioFormatIDPreference.isEmpty()){ bestAudioFormat = if (audioFormatIDPreference.isEmpty()){
infoUtil.getGenericAudioFormats(resources).last() infoUtil.getGenericAudioFormats(resources).first()
}else{ }else{
Format( Format(
audioFormatIDPreference.first().split("+").first(), audioFormatIDPreference.first().split("+").first(),
@ -204,132 +206,24 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return repository.getItemByID(id) return repository.getItemByID(id)
} }
fun getAllByIDs(ids: List<Long>) : List<DownloadItem> {
return repository.getAllItemsByIDs(ids)
}
fun getHistoryItemById(id: Long) : HistoryItem? { fun getHistoryItemById(id: Long) : HistoryItem? {
return historyRepository.getItem(id) return historyRepository.getItem(id)
} }
fun getDownloadType(t: Type? = null, url: String) : Type { fun getDownloadType(t: Type? = null, url: String) : Type {
var type = t return sharedDownloadViewModel.getDownloadType(t, url)
if (type == null){
val preferredDownloadType = sharedPreferences.getString("preferred_download_type", Type.auto.toString())
type = if (sharedPreferences.getBoolean("remember_download_type", false)){
Type.valueOf(sharedPreferences.getString("last_used_download_type",
preferredDownloadType)!!)
}else{
Type.valueOf(preferredDownloadType!!)
}
}
return when(type){
Type.auto -> {
if (urlsForAudioType.any { url.contains(it) }){
Type.audio
}else{
Type.video
}
}
else -> type
}
} }
fun createDownloadItemFromResult(result: ResultItem?, url: String = "", givenType: Type) : DownloadItem { fun createDownloadItemFromResult(result: ResultItem?, url: String = "", givenType: Type) : DownloadItem {
val resultItem = result ?: createEmptyResultItem(url) return sharedDownloadViewModel.createDownloadItemFromResult(result, url, givenType)
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 { fun createResultItemFromDownload(downloadItem: DownloadItem) : ResultItem {
return ResultItem( return sharedDownloadViewModel.createResultItemFromDownload(downloadItem)
0,
downloadItem.url,
downloadItem.title,
downloadItem.author,
downloadItem.duration,
downloadItem.thumb,
downloadItem.website,
downloadItem.playlistTitle,
downloadItem.allFormats,
"",
arrayListOf(),
downloadItem.playlistURL,
downloadItem.playlistIndex,
System.currentTimeMillis()
)
} }
fun createResultItemFromHistory(downloadItem: HistoryItem) : ResultItem { fun createResultItemFromHistory(downloadItem: HistoryItem) : ResultItem {
@ -353,23 +247,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun createEmptyResultItem(url: String) : ResultItem { fun createEmptyResultItem(url: String) : ResultItem {
return ResultItem( return sharedDownloadViewModel.createEmptyResultItem(url)
0,
url,
"",
"",
"",
"",
"",
"",
arrayListOf(),
"",
arrayListOf(),
"",
null,
System.currentTimeMillis()
)
} }
fun switchDownloadType(list: List<DownloadItem>, type: Type) : List<DownloadItem>{ fun switchDownloadType(list: List<DownloadItem>, type: Type) : List<DownloadItem>{
@ -471,166 +349,140 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> { fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf() return sharedDownloadViewModel.getPreferredVideoRequirements()
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 //requirement and importance
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> { fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf() return sharedDownloadViewModel.getPreferredVideoRequirements()
//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
} }
fun getFormat(formats: List<Format>, type: Type) : Format { fun getFormat(formats: List<Format>, type: Type) : Format {
when(type) { return sharedDownloadViewModel.getFormat(formats, 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>{ fun getPreferredAudioFormats(formats: List<Format>) : ArrayList<String>{
val preferredAudioFormats = arrayListOf<String>() return sharedDownloadViewModel.getPreferredAudioFormats(formats)
for (f in formats.sortedBy { it.format_id }){
val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
if (fId != null) {
if (fId.split("+").all { formats.map { f-> f.format_id }.contains(it) }){
preferredAudioFormats.addAll(fId.split("+"))
break
}
}
}
if (preferredAudioFormats.isEmpty()){
val audioF = getFormat(formats, Type.audio)
if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){
preferredAudioFormats.add(audioF.format_id)
}
}
return preferredAudioFormats
} }
fun generateCommandFormat(c: CommandTemplate) : Format { fun generateCommandFormat(c: CommandTemplate) : Format {
return Format( return sharedDownloadViewModel.generateCommandFormat(c)
c.title,
c.id.toString(),
"",
"",
"",
0,
c.content.replace("\n", " ")
)
} }
data class ProcessingItemsJob(
var jobID: Long = 0,
var job: Job? = null,
var itemType: String,
var itemIDs: List<Long>
)
val loadingProcessingItems: MutableStateFlow<Boolean> = MutableStateFlow(false)
fun getLatestCommandTemplateAsFormat() : Format { var loadingProcessingDownloadsJobs: MutableStateFlow<List<ProcessingItemsJob>> = MutableStateFlow(listOf())
val t = commandTemplateDao.getFirst()!! private fun cancelAllLoadingProcessingDownloads() {
return Format(t.title, "", "", "", "", 0, t.content) loadingProcessingDownloadsJobs.value.onEach {
} it.job?.cancel(CancellationException())
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
)))
} }
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){ 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) { fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) {
cancelAllLoadingProcessingDownloads()
repository.cancelActiveQueued() repository.cancelActiveQueued()
} }
@ -706,11 +559,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dao.getSavedDownloadsList() 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>{ fun getActiveDownloads() : List<DownloadItem>{
return repository.getActiveDownloads() return repository.getActiveDownloads()
} }
@ -725,6 +573,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch { suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.resetScheduleTimeForItems(items) dbManager.downloadDao.resetScheduleTimeForItems(items)
dbManager.downloadDao.resetPausedItems()
repository.startDownloadWorker(emptyList(), application) repository.startDownloadWorker(emptyList(), application)
} }
@ -789,173 +638,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao.updateDownloadID(-current, id) 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) dbManager.downloadDao.reQueueDownloadItems(items)
repository.startDownloadWorker(emptyList(), application) repository.startDownloadWorker(emptyList(), application)
} }
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : Pair<List<Long>, List<Long>> { suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : List<SharedDownloadViewModel.AlreadyExistsIDs> {
val context = App.instance val ids = sharedDownloadViewModel.queueDownloads(items, ign)
val alarmScheduler = AlarmScheduler(context) return ids
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)
} }
fun getQueuedCollectedFileSize() : Long { fun getQueuedCollectedFileSize() : Long {
@ -978,19 +668,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao.updateProcessingtoSavedStatus() 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> { suspend fun updateProcessingFormat(selectedFormats: List<FormatTuple>): List<Long> {
val items = repository.getProcessingDownloads() val items = repository.getProcessingDownloads()
items.forEachIndexed { index, i -> items.forEachIndexed { index, i ->
@ -1098,16 +775,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val first = dao.getFirstProcessingDownload() 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> { fun getURLsByStatus(list: List<DownloadRepository.Status>) : List<String> {
return dao.getURLsByStatus(list.map { it.toString() }) return dao.getURLsByStatus(list.map { it.toString() })
} }

View file

@ -10,6 +10,7 @@ import androidx.preference.PreferenceManager
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.Data import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequest import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
@ -124,7 +125,15 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
} }
//schedule for next time //schedule for next time
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workConstraints = Constraints.Builder() val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>() val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
.addTag("observeSources") .addTag("observeSources")
.addTag(it.id.toString()) .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.View
import android.view.WindowManager import android.view.WindowManager
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -34,9 +35,11 @@ import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.CookieViewModel import com.deniscerri.ytdl.database.viewmodel.CookieViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel 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.ResultViewModel
import com.deniscerri.ytdl.ui.BaseActivity import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.util.ThemeUtil import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -52,6 +55,7 @@ class ShareActivity : BaseActivity() {
lateinit var context: Context lateinit var context: Context
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var historyViewModel: HistoryViewModel
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
private lateinit var cookieViewModel: CookieViewModel private lateinit var cookieViewModel: CookieViewModel
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
@ -115,6 +119,7 @@ class ShareActivity : BaseActivity() {
context = baseContext context = baseContext
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java] cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
@ -208,6 +213,18 @@ class ShareActivity : BaseActivity() {
} }
this@ShareActivity.finish() 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.appcompat.view.ActionMode
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.os.bundleOf
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -59,12 +60,14 @@ import com.google.android.material.search.SearchBar
import com.google.android.material.search.SearchView import com.google.android.material.search.SearchView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.* import java.util.*
import kotlin.collections.ArrayList
class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggestionsAdapter.OnItemClickListener, OnClickListener { 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 downloadSelectedFab: ExtendedFloatingActionButton? = null
private var downloadAllFab: ExtendedFloatingActionButton? = null private var downloadAllFab: ExtendedFloatingActionButton? = null
private var clipboardFab: ExtendedFloatingActionButton? = null private var clipboardFab: ExtendedFloatingActionButton? = null
private var homeFabs: CoordinatorLayout? = null private var homeFabs: LinearLayout? = null
private var infoUtil: InfoUtil? = null private var infoUtil: InfoUtil? = null
private var downloadQueue: ArrayList<ResultItem>? = null private var downloadQueue: ArrayList<ResultItem>? = null
@ -216,12 +219,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
inputQueries!!.addAll(argList) inputQueries!!.addAll(argList)
} }
if(arguments?.getBoolean("search") == true){
requireView().post {
searchBar?.performClick()
}
}
if (inputQueries != null) { if (inputQueries != null) {
lifecycleScope.launch(Dispatchers.IO){ lifecycleScope.launch(Dispatchers.IO){
resultViewModel.deleteAll() resultViewModel.deleteAll()
@ -276,22 +273,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
lifecycleScope.launch { lifecycleScope.launch {
launch{ launch{
downloadViewModel.alreadyExistsUiState.collectLatest { res -> downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.downloadItems.isNotEmpty() || res.historyItems.isNotEmpty()) { if (res.isNotEmpty()){
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
kotlin.runCatching { val bundle = bundleOf(
UiUtil.handleExistingDownloadsResponse( Pair("duplicates", ArrayList(res))
requireActivity(), )
requireActivity().lifecycleScope, delay(500)
requireActivity().supportFragmentManager, findNavController().navigate(R.id.downloadsAlreadyExistDialog, bundle)
res,
downloadViewModel,
historyViewModel)
}
} }
downloadViewModel.alreadyExistsUiState.value = DownloadViewModel.AlreadyExistsUIState( downloadViewModel.alreadyExistsUiState.value = mutableListOf()
mutableListOf(),
mutableListOf()
)
} }
} }
} }
@ -313,15 +303,24 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
arguments?.remove("showDownloadsWithUpdatedFormats") arguments?.remove("showDownloadsWithUpdatedFormats")
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val ids = arguments?.getLongArray("downloadIds") ?: return@launch val ids = arguments?.getLongArray("downloadIds") ?: return@launch
downloadViewModel.updateItemsWithIdsToProcessingStatus(ids.toList()) val jobID = downloadViewModel.turnDownloadItemsToProcessingDownloads(ids.toList())
withContext(Dispatchers.Main){ 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){ if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){
updateSearchViewItems(searchView?.editText?.text) updateSearchViewItems(searchView?.editText?.text.toString())
} }
requireView().post { requireView().post {
@ -405,13 +404,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
chipGroupDivider?.visibility = VISIBLE chipGroupDivider?.visibility = VISIBLE
} }
updateSearchViewItems(searchView!!.editText.text) updateSearchViewItems(searchView!!.editText.text.toString())
} }
} }
searchView!!.editText.doAfterTextChanged { searchView!!.editText.doAfterTextChanged {
if (searchView!!.currentTransitionState != SearchView.TransitionState.SHOWN) return@doAfterTextChanged if (searchView!!.currentTransitionState != SearchView.TransitionState.SHOWN) return@doAfterTextChanged
updateSearchViewItems(it) updateSearchViewItems(it.toString())
} }
searchView!!.editText.setOnTouchListener(OnTouchListener { _, event -> searchView!!.editText.setOnTouchListener(OnTouchListener { _, event ->
@ -454,7 +453,11 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
searchBar!!.setOnMenuItemClickListener { m: MenuItem -> searchBar!!.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) { when (m.itemId) {
R.id.delete_results -> { R.id.delete_results -> {
resultViewModel.cancelParsingQueries() lifecycleScope.launch {
withContext(Dispatchers.IO){
resultViewModel.cancelParsingQueries()
}
}
resultViewModel.getTrending() resultViewModel.getTrending()
selectedObjects = ArrayList() selectedObjects = ArrayList()
searchBar!!.setText("") searchBar!!.setText("")
@ -479,7 +482,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} }
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private fun updateSearchViewItems(searchQuery: Editable?) = lifecycleScope.launch(Dispatchers.Main) { private fun updateSearchViewItems(searchQuery: String) = lifecycleScope.launch(Dispatchers.Main) {
lifecycleScope.launch { lifecycleScope.launch {
if (searchView!!.editText.text.isEmpty()){ if (searchView!!.editText.text.isEmpty()){
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
@ -490,13 +493,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
val combinedList = mutableListOf<SearchSuggestionItem>() val combinedList = mutableListOf<SearchSuggestionItem>()
val history = withContext(Dispatchers.IO){ val history = withContext(Dispatchers.IO){
resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery!!) } resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery) }
}.map { }.map {
SearchSuggestionItem(it, SearchSuggestionType.HISTORY) SearchSuggestionItem(it, SearchSuggestionType.HISTORY)
} }
val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){ val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
infoUtil!!.getSearchSuggestions(searchQuery.toString()) infoUtil!!.getSearchSuggestions(searchQuery)
} }
}else{ }else{
emptyList() emptyList()
@ -509,12 +512,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
val url = checkClipboard() val url = checkClipboard()
url?.apply { 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)) combinedList.add(0, SearchSuggestionItem(this.joinToString("\n"), SearchSuggestionType.CLIPBOARD))
} }
} }
searchSuggestionsAdapter?.submitList(combinedList) searchSuggestionsAdapter?.submitList(combinedList)
// history.forEach { s -> // history.forEach { s ->
@ -729,21 +732,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} catch (e: Exception) {""} } catch (e: Exception) {""}
if (viewIdName.isNotEmpty()) { if (viewIdName.isNotEmpty()) {
if (viewIdName == "downloadAll") { if (viewIdName == "downloadAll") {
lifecycleScope.launch { val showDownloadCard = sharedPreferences!!.getBoolean("download_card", true)
val downloadList = withContext(Dispatchers.IO){ val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(resultsList!!.map { it!!.id }, downloadNow = !showDownloadCard)
downloadViewModel.turnResultItemsToDownloadItems(resultsList!!) if (showDownloadCard){
} findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
if (sharedPreferences!!.getBoolean("download_card", true)) { Pair("processingDownloadsJobID", jobID)
CoroutineScope(Dispatchers.IO).launch { ))
downloadViewModel.insertToProcessing(downloadList)
}
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
} else {
downloadList.chunked(100).forEach {
downloadViewModel.queueDownloads(it)
}
}
} }
} }
} }
@ -806,24 +800,18 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} }
R.id.download -> { R.id.download -> {
lifecycleScope.launch { lifecycleScope.launch {
if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects.size == 1) { val showDownloadCard = sharedPreferences!!.getBoolean("download_card", true)
if (showDownloadCard && selectedObjects.size == 1) {
showSingleDownloadSheet( showSingleDownloadSheet(
selectedObjects[0], selectedObjects[0],
downloadViewModel.getDownloadType(url = selectedObjects[0].url) downloadViewModel.getDownloadType(url = selectedObjects[0].url)
) )
}else{ }else{
val downloadList = withContext(Dispatchers.IO){ val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(selectedObjects.map { it.id }, downloadNow = !showDownloadCard)
downloadViewModel.turnResultItemsToDownloadItems(selectedObjects) if (showDownloadCard){
} findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
Pair("processingDownloadsJobID", jobID)
if (sharedPreferences!!.getBoolean("download_card", true)) { ))
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.insertToProcessing(downloadList)
}
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
} else {
downloadViewModel.queueDownloads(downloadList)
} }
} }
clearCheckedItems() clearCheckedItems()
@ -835,7 +823,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
homeAdapter?.checkAll(resultsList) homeAdapter?.checkAll(resultsList)
selectedObjects.clear() selectedObjects.clear()
resultsList?.forEach { selectedObjects.add(it!!) } 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 true
} }
R.id.invert_selected -> { R.id.invert_selected -> {
@ -905,14 +893,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} }
override fun onSearchSuggestionAdd(t: String) { override fun onSearchSuggestionAdd(text: String) {
val items = t.split("\n") val items = text.split("\n")
items.forEach {text -> items.forEach {t ->
val present = queriesChipGroup!!.children.firstOrNull { (it as Chip).text.toString() == text } val present = queriesChipGroup!!.children.firstOrNull { (it as Chip).text.toString() == t }
if (present == null) { if (present == null) {
val chip = layoutinflater!!.inflate(R.layout.input_chip, queriesChipGroup, false) as Chip 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.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK))
chip.setOnClickListener { chip.setOnClickListener {
if (queriesChipGroup!!.childCount == 1) queriesConstraint!!.visibility = View.GONE if (queriesChipGroup!!.childCount == 1) queriesConstraint!!.visibility = View.GONE
@ -924,17 +912,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} }
searchView!!.editText.setText("") searchView!!.editText.setText("")
if (queriesChipGroup!!.childCount == 0) queriesConstraint!!.visibility = View.GONE queriesConstraint?.isVisible = queriesChipGroup?.childCount!! > 0
else queriesConstraint!!.visibility = View.VISIBLE
val clipBoardItem = searchSuggestionsRecyclerView?.layoutManager?.findViewByPosition(0) searchSuggestionsAdapter?.getList()?.apply {
clipBoardItem?.apply { if (this.first().type == SearchSuggestionType.CLIPBOARD){
if ((this as ConstraintLayout).findViewById<TextView>(R.id.suggestion_text).text == getString(R.string.link_you_copied)){ val newList = this.toMutableList().drop(1)
searchSuggestionsAdapter?.notifyItemRemoved(0) searchSuggestionsAdapter?.submitList(newList)
} }
} }
} }
override fun onSearchSuggestionLongClick(text: String, position: Int) { 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.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
resultViewModel.removeSearchQueryFromHistory(text) resultViewModel.removeSearchQueryFromHistory(text)
updateSearchViewItems(searchView!!.editText.text) updateSearchViewItems(searchView!!.editText.text.toString())
} }
deleteDialog.show() deleteDialog.show()
} }

View file

@ -2,27 +2,34 @@ package com.deniscerri.ytdl.ui.adapter
import android.app.Activity import android.app.Activity
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import android.widget.PopupMenu
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.AlreadyExistsItem
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.Extensions.loadThumbnail import com.deniscerri.ytdl.util.Extensions.loadThumbnail
import com.deniscerri.ytdl.util.Extensions.popup import com.deniscerri.ytdl.util.Extensions.popup
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.card.MaterialCardView 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 DIFF_CALLBACK
).build()) { ).build()) {
private val onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener
@ -39,7 +46,7 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
val cardView: MaterialCardView val cardView: MaterialCardView
init { 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) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val alreadyExistsItem = getItem(position) ?: return val alreadyExistsItem = getItem(position) ?: return
val item = alreadyExistsItem.first
val historyID = alreadyExistsItem.second
val card = holder.cardView val card = holder.cardView
card.tag = item.id.toString() card.tag = alreadyExistsItem.downloadItem.id.toString()
card.popup() val item = alreadyExistsItem.downloadItem
val uiHandler = Handler(Looper.getMainLooper()) val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view) 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 } 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) val formatNote = card.findViewById<TextView>(R.id.format_note)
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility = if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
View.GONE View.GONE
@ -102,45 +120,64 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
if (fileSizeReadable == "?") fileSize.visibility = View.GONE if (fileSizeReadable == "?") fileSize.visibility = View.GONE
else fileSize.text = fileSizeReadable else fileSize.text = fileSizeReadable
val editBtn = card.findViewById<Button>(R.id.already_exists_edit) val menu = card.findViewById<View>(R.id.options)
val deleteBtn = card.findViewById<Button>(R.id.already_exists_delete) 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 { card.setOnLongClickListener {
onItemClickListener.onDeleteItem(item, position, historyID) onItemClickListener.onDeleteItem(alreadyExistsItem, position)
true true
} }
editBtn.setOnClickListener { if (alreadyExistsItem.historyID != null){
onItemClickListener.onEditItem(item, position)
}
deleteBtn.setOnClickListener {
onItemClickListener.onDeleteItem(item, position, historyID)
}
card.setOnClickListener(null)
if (historyID != null){
card.setOnClickListener { card.setOnClickListener {
onItemClickListener.onShowHistoryItem(historyID) onItemClickListener.onShowHistoryItem(alreadyExistsItem.historyID!!)
} }
}else{
card.setOnClickListener(null)
} }
} }
interface OnItemClickListener { interface OnItemClickListener {
fun onEditItem(downloadItem: DownloadItem, position: Int) fun onEditItem(alreadyExistsItem: AlreadyExistsItem, position: Int)
fun onDeleteItem(downloadItem: DownloadItem, position: Int, historyID: Long?) fun onDeleteItem(alreadyExistsItem: AlreadyExistsItem, position: Int)
fun onShowHistoryItem(id: Long) fun onShowHistoryItem(historyItemID: Long)
} }
companion object { companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<Pair<DownloadItem, Long?>> = object : DiffUtil.ItemCallback<Pair<DownloadItem, Long?>>() { private val DIFF_CALLBACK: DiffUtil.ItemCallback<AlreadyExistsItem> = object : DiffUtil.ItemCallback<AlreadyExistsItem>() {
override fun areItemsTheSame(oldItem: Pair<DownloadItem, Long?>, newItem: Pair<DownloadItem, Long?>): Boolean { override fun areItemsTheSame(oldItem: AlreadyExistsItem, newItem: AlreadyExistsItem): Boolean {
return oldItem.first.id == newItem.first.id return oldItem.downloadItem.id == newItem.downloadItem.id
} }
override fun areContentsTheSame(oldItem: Pair<DownloadItem, Long?>, newItem: Pair<DownloadItem, Long?>): Boolean { override fun areContentsTheSame(oldItem: AlreadyExistsItem, newItem: AlreadyExistsItem): 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 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 if (item == null) return
card.tag = item.id.toString() card.tag = item.id.toString()
holder.itemView.tag = item.id.toString()
//Scheduled Time //Scheduled Time
val time = mainView.findViewById<TextView>(R.id.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) { if (checkedItems.size > 0 || inverted) {
checkCard(card, item.id, position) checkCard(card, item.id, position)
} else { } else {
onItemClickListener.onCardClick(item.id) onItemClickListener.onCardClick(item.id, position)
} }
} }
@ -218,7 +219,7 @@ class ScheduledDownloadAdapter(onItemClickListener: OnItemClickListener, activit
interface OnItemClickListener { interface OnItemClickListener {
fun onActionButtonClick(itemID: Long) fun onActionButtonClick(itemID: Long)
fun onCardClick(itemID: Long) fun onCardClick(itemID: Long, position: Int)
fun onCardSelect(isChecked: Boolean, 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 { 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 val linear = holder.linear
when (item.type){ when (item.type){
SearchSuggestionType.SUGGESTION -> { SearchSuggestionType.SUGGESTION -> {
holder.itemView.tag = SearchSuggestionType.SUGGESTION.toString()
val textView = linear.findViewById<TextView>(R.id.suggestion_text) val textView = linear.findViewById<TextView>(R.id.suggestion_text)
textView.text = item.text textView.text = item.text
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_search, 0, 0, 0) textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_search, 0, 0, 0)
@ -67,6 +68,7 @@ class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activit
} }
} }
SearchSuggestionType.HISTORY -> { SearchSuggestionType.HISTORY -> {
holder.itemView.tag = SearchSuggestionType.HISTORY.toString()
val textView = linear.findViewById<TextView>(R.id.suggestion_text) val textView = linear.findViewById<TextView>(R.id.suggestion_text)
textView.text = item.text textView.text = item.text
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0) textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0)
@ -85,6 +87,7 @@ class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activit
} }
} }
SearchSuggestionType.CLIPBOARD -> { SearchSuggestionType.CLIPBOARD -> {
holder.itemView.tag = SearchSuggestionType.CLIPBOARD.toString()
val textView = linear.findViewById<TextView>(R.id.suggestion_text) val textView = linear.findViewById<TextView>(R.id.suggestion_text)
textView.text = activity.getString(R.string.link_you_copied) textView.text = activity.getString(R.string.link_you_copied)
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_language, 0, 0, 0) 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 { interface OnItemClickListener {
fun onSearchSuggestionClick(text: String) 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.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.* import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children import androidx.core.view.children
@ -28,12 +30,17 @@ import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.ChapterItem import com.deniscerri.ytdl.database.models.ChapterItem
import com.deniscerri.ytdl.database.models.DownloadItem 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.setTextAndRecalculateWidth
import com.deniscerri.ytdl.util.Extensions.toStringDuration 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.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.VideoPlayerUtil import com.deniscerri.ytdl.util.VideoPlayerUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
@ -55,6 +62,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.reflect.Type import java.lang.reflect.Type
import java.util.* import java.util.*
import kotlin.math.min
import kotlin.properties.Delegates import kotlin.properties.Delegates
@ -68,10 +76,11 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
private lateinit var progress : ProgressBar private lateinit var progress : ProgressBar
private lateinit var pauseBtn : MaterialButton private lateinit var pauseBtn : MaterialButton
private lateinit var rewindBtn : MaterialButton private lateinit var rewindBtn : MaterialButton
private lateinit var forwardBtn : MaterialButton
private lateinit var muteBtn : MaterialButton private lateinit var muteBtn : MaterialButton
private lateinit var rangeSlider : RangeSlider private lateinit var rangeSlider : RangeSlider
private lateinit var fromTextInput : TextInputLayout private lateinit var fromTextInput : EditText
private lateinit var toTextInput : TextInputLayout private lateinit var toTextInput : EditText
private lateinit var cancelBtn : Button private lateinit var cancelBtn : Button
private lateinit var okBtn : Button private lateinit var okBtn : Button
private lateinit var forceKeyframes: MaterialSwitch 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 suggestedLabel : View
private lateinit var item: DownloadItem private lateinit var item: DownloadItem
private var timeSeconds by Delegates.notNull<Int>() private var itemDurationTimestamp = 0L
private lateinit var selectedCuts: MutableList<String> private lateinit var selectedCuts: MutableList<String>
override fun onCreate(savedInstanceState: Bundle?) { 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 frame = view.findViewById<MaterialCardView>(R.id.frame_layout)
val videoView = view.findViewById<PlayerView>(R.id.video_view) val videoView = view.findViewById<PlayerView>(R.id.video_view)
videoView.player = player videoView.player = player
timeSeconds = convertStringToTimestamp(item.duration) itemDurationTimestamp = item.duration.convertToTimestamp()
if (chapters == null) chapters = emptyList() if (chapters == null) chapters = emptyList()
//cut section //cut section
@ -131,10 +140,11 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
progress = view.findViewById(R.id.progress) progress = view.findViewById(R.id.progress)
pauseBtn = view.findViewById(R.id.pause) pauseBtn = view.findViewById(R.id.pause)
rewindBtn = view.findViewById(R.id.rewind) rewindBtn = view.findViewById(R.id.rewind)
forwardBtn = view.findViewById(R.id.forward)
muteBtn = view.findViewById(R.id.mute) muteBtn = view.findViewById(R.id.mute)
rangeSlider = view.findViewById(R.id.rangeSlider) rangeSlider = view.findViewById(R.id.rangeSlider)
fromTextInput = view.findViewById(R.id.from_textinput) fromTextInput = view.findViewById(R.id.from_textinput_edittext)
toTextInput = view.findViewById(R.id.to_textinput) toTextInput = view.findViewById(R.id.to_textinput_edittext)
cancelBtn = view.findViewById(R.id.cancelButton) cancelBtn = view.findViewById(R.id.cancelButton)
okBtn = view.findViewById(R.id.okButton) okBtn = view.findViewById(R.id.okButton)
suggestedChips = view.findViewById(R.id.chapters) suggestedChips = view.findViewById(R.id.chapters)
@ -216,12 +226,12 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
videoProgress(player).collect { p -> videoProgress(player).collect { p ->
val currentTime = p.toStringDuration(Locale.US) val currentTime = p.toStringDuration(Locale.US)
durationText.text = "$currentTime / ${item.duration}" durationText.text = "$currentTime / ${item.duration}"
val startTimestamp = convertStringToTimestamp(fromTextInput.editText!!.text.toString()) val startTimestamp = fromTextInput.text.toString().convertToTimestamp()
if (toTextInput.editText!!.text.isNotBlank()){ if (toTextInput.text.isNotBlank()){
val endTimestamp = convertStringToTimestamp(toTextInput.editText!!.text.toString()) val endTimestamp = toTextInput.text.toString().convertToTimestamp()
if (p >= endTimestamp){ if (p >= endTimestamp / 1000 || (!player.isPlaying && p >= endTimestamp / 1000 - 1)){
player.prepare() player.prepare()
player.seekTo((startTimestamp * 1000).toLong()) player.seekTo(startTimestamp)
} }
} }
@ -260,12 +270,22 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
rewindBtn.setOnClickListener { rewindBtn.setOnClickListener {
try { try {
val seconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString()) val stmp = fromTextInput.text.toString().convertToTimestamp()
player.seekTo((seconds * 1000).toLong()) player.seekTo(stmp)
player.play() player.play()
}catch (ignored: Exception) {} }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 prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = prefs.edit() val editor = prefs.edit()
@ -279,12 +299,12 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
@SuppressLint("SetTextI18n", "ClickableViewAccessibility") @SuppressLint("SetTextI18n", "ClickableViewAccessibility")
private fun initCutSection(){ private fun initCutSection(){
fromTextInput.editText!!.setTextAndRecalculateWidth("0:00") fromTextInput.setTextAndRecalculateWidth("0:00")
toTextInput.editText!!.setTextAndRecalculateWidth(item.duration) toTextInput.setTextAndRecalculateWidth(item.duration)
rangeSlider.valueFrom = 0f rangeSlider.valueFrom = 0f
rangeSlider.valueTo = timeSeconds.toFloat() rangeSlider.valueTo = (itemDurationTimestamp / 1000).toFloat()
rangeSlider.setValues(0F, timeSeconds.toFloat()) rangeSlider.setValues(0F, (itemDurationTimestamp / 1000).toFloat())
rangeSlider.setOnTouchListener { _, event -> // Handle touch events here rangeSlider.setOnTouchListener { _, event -> // Handle touch events here
when (event.action) { when (event.action) {
MotionEvent.ACTION_MOVE -> { 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 // Return 'false' to allow the event to continue propagating or 'true' to consume it
false 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 { override fun onKey(p0: View?, keyCode: Int, event: KeyEvent?): Boolean {
if ((event!!.action == KeyEvent.ACTION_DOWN) && if ((event!!.action == KeyEvent.ACTION_DOWN) &&
(keyCode == KeyEvent.KEYCODE_ENTER)) { (keyCode == KeyEvent.KEYCODE_ENTER)) {
var startTimestamp = rangeSlider.valueFrom.toInt() var startTimestamp = rangeSlider.valueFrom.toInt()
val endTimestamp = rangeSlider.valueTo.toInt()
fromTextInput.editText!!.clearFocus() fromTextInput.clearFocus()
var seconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString()) var timestamp = fromTextInput.text.toString().convertToTimestamp()
val endSeconds = convertStringToTimestamp(toTextInput.editText!!.text.toString()) val endstamp = toTextInput.text.toString().convertToTimestamp()
if (seconds == 0) { if (timestamp == 0L) {
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US)) fromTextInput.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
}else if (seconds > endSeconds){ }else if (timestamp > endstamp){
startTimestamp = 0 startTimestamp = 0
seconds = 0 timestamp = 0
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US)) 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()) rangeSlider.setValues((timestamp / 1000).toFloat(), (endstamp / 1000).toFloat())
okBtn.isEnabled = seconds != 0 || endSeconds != timeSeconds okBtn.isEnabled = timestamp != 0L || endstamp != itemDurationTimestamp
try { try {
player.seekTo(seconds.toLong() * 1000) player.seekTo(timestamp)
player.play() player.play()
}catch (ignored: Exception) {} }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 { override fun onKey(p0: View?, keyCode: Int, event: KeyEvent?): Boolean {
if ((event!!.action == KeyEvent.ACTION_DOWN) && if ((event!!.action == KeyEvent.ACTION_DOWN) &&
(keyCode == KeyEvent.KEYCODE_ENTER)) { (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() if (endstamp > itemDurationTimestamp){
val startSeconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString()) endstamp = itemDurationTimestamp
var endSeconds = convertStringToTimestamp(toTextInput.editText!!.text.toString()) }
if (endstamp == 0L) {
if (endSeconds > timeSeconds){ endstamp = itemDurationTimestamp
endTimestamp = timeSeconds }
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US)) if (endstamp <= timestamp){
endSeconds = timeSeconds endstamp = timestamp + 1000
} }
if (endSeconds == 0) { toTextInput.setTextAndRecalculateWidth(endstamp.toStringTimeStamp())
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
}else if (endSeconds <= rangeSlider.valueFrom.toInt()){
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
}
toTextInput.editText!!.setTextAndRecalculateWidth(toTextInput.editText!!.text.toString()) rangeSlider.setValues((timestamp / 1000).toFloat(), (endstamp / 1000).toFloat())
okBtn.isEnabled = timestamp != 0L || endstamp != itemDurationTimestamp
rangeSlider.setValues(startSeconds.toFloat(), endSeconds.toFloat())
okBtn.isEnabled = startSeconds != 0 || endSeconds != timeSeconds
try { try {
player.seekTo((endSeconds.toLong() - 2L) * 1000) player.seekTo(endstamp - 1500)
player.play() player.play()
}catch (ignored: Exception) {} }catch (ignored: Exception) {}
@ -391,7 +448,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
okBtn.isEnabled = false okBtn.isEnabled = false
okBtn.setOnClickListener { okBtn.setOnClickListener {
forceKeyframes.isVisible = true forceKeyframes.isVisible = true
val chip = createChip("${fromTextInput.editText!!.text}-${toTextInput.editText!!.text}") val chip = createChip("${fromTextInput.text}-${toTextInput.text}")
chip.performClick() chip.performClick()
cutSection.visibility = View.GONE cutSection.visibility = View.GONE
cutListSection.visibility = View.VISIBLE cutListSection.visibility = View.VISIBLE
@ -402,30 +459,30 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
private fun updateFromSlider(){ private fun updateFromSlider(){
val values = rangeSlider.values val values = rangeSlider.values
val startTimestamp = values[0].toInt() val startTimestamp = values[0].toLong() * 1000
val endTimestamp = values[1].toInt() val endTimestamp = values[1].toLong() * 1000
val startTimestampString = startTimestamp.toStringDuration(Locale.US) val startTimestampString = startTimestamp.toStringTimeStamp()
val endTimestampString = endTimestamp.toStringDuration(Locale.US) val endTimestampString = endTimestamp.toStringTimeStamp()
var draggedFromBeginning = true var draggedFromBeginning = true
if (toTextInput.editText!!.text.toString() != endTimestampString){ if (toTextInput.text.toString() != endTimestampString){
draggedFromBeginning = false draggedFromBeginning = false
} }
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestampString) fromTextInput.setTextAndRecalculateWidth(startTimestampString)
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestampString) 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 { try {
if (draggedFromBeginning){ if (draggedFromBeginning){
player.seekTo(startpos) player.seekTo(startpos)
}else{ }else{
var endpos = (endTimestamp * 1000).toLong() - 1500 var endpos = (endTimestamp * 1000) - 1500
if (endpos < (startTimestamp * 1000).toLong()) endpos = startpos if (endpos < (startTimestamp * 1000)) endpos = startpos
player.seekTo(endpos) player.seekTo(endpos)
} }
player.play() player.play()
@ -466,7 +523,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
newCutBtn.setOnClickListener { newCutBtn.setOnClickListener {
cutSection.visibility = View.VISIBLE cutSection.visibility = View.VISIBLE
cutListSection.visibility = View.GONE cutListSection.visibility = View.GONE
rangeSlider.setValues(0F, timeSeconds.toFloat()) rangeSlider.setValues(0F, ( itemDurationTimestamp / 1000).toFloat())
player.seekTo(0) player.seekTo(0)
suggestedChips.children.apply { suggestedChips.children.apply {
this.forEach { this.forEach {
@ -495,8 +552,8 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
} }
private fun createChip(timestamp: String) : Chip { private fun createChip(timestamp: String) : Chip {
val startTimestamp = convertStringToTimestamp(timestamp.split("-")[0].replace(";", "")) val startTimestamp = timestamp.split("-")[0].replace(";", "").convertToTimestamp()
val endTimestamp = convertStringToTimestamp(timestamp.split("-")[1].replace(";", "")) val endTimestamp = timestamp.split("-")[1].replace(";", "").convertToTimestamp()
val chip = layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip val chip = layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
chip.text = timestamp chip.text = timestamp
@ -508,9 +565,9 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
chip.setOnClickListener { chip.setOnClickListener {
if (chip.isChecked) { if (chip.isChecked) {
rangeSlider.setValues(startTimestamp.toFloat(), endTimestamp.toFloat()) rangeSlider.setValues((startTimestamp / 1000).toFloat(), (endTimestamp / 1000).toFloat())
player.prepare() player.prepare()
player.seekTo((startTimestamp * 1000).toLong()) player.seekTo(startTimestamp)
player.play() player.play()
}else { }else {
player.seekTo(0) player.seekTo(0)
@ -600,21 +657,95 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
} }
}.flowOn(Dispatchers.Main) }.flowOn(Dispatchers.Main)
private fun convertStringToTimestamp(duration: String): Int { // private fun showTimestampBottomSheet(startTimestamp: Boolean, currentTimestamp: Long, onChange: (value: Long) -> Unit){
return try { // val bottomSheet = BottomSheetDialog(requireContext())
val timeArray = duration.split(":") // bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
var timeSeconds = timeArray[timeArray.lastIndex].toInt() // bottomSheet.setContentView(R.layout.adjust_cut_timestamp)
var times = 60 //
for (i in timeArray.lastIndex - 1 downTo 0) { // val hours = bottomSheet.findViewById<NumberPicker>(R.id.hours)!!
timeSeconds += timeArray[i].toInt() * times // val minutes = bottomSheet.findViewById<NumberPicker>(R.id.minutes)!!
times *= 60 // val seconds = bottomSheet.findViewById<NumberPicker>(R.id.seconds)!!
} // val milliseconds = bottomSheet.findViewById<NumberPicker>(R.id.milliseconds)!!
timeSeconds //
}catch (e: Exception){ // val current = currentTimestamp.toTimePeriodsArray()
e.printStackTrace() //
0 // 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) { override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog) super.onCancel(dialog)

View file

@ -6,6 +6,7 @@ import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -17,6 +18,7 @@ import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.setPadding import androidx.core.view.setPadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -45,6 +47,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File 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 { 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{ }else{
//open multi download card instead //open multi download card instead
if (activity is ShareActivity){ if (activity is ShareActivity){
val preferredType = DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!)
findNavController().navigate(R.id.action_downloadBottomSheetDialog_to_selectPlaylistItemsDialog, bundleOf( findNavController().navigate(R.id.action_downloadBottomSheetDialog_to_selectPlaylistItemsDialog, bundleOf(
Pair("results", result), Pair("resultIDs", result.map { it!!.id }.toLongArray()),
Pair("type", preferredType),
)) ))
}else{ }else{
dismiss() dismiss()

View file

@ -91,7 +91,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
preferences.edit().putString("lastCommandTemplateUsed", downloadItem.format.format_note).apply() 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.format = downloadViewModel.generateCommandFormat(CommandTemplate(0,"txt", "-a \"${downloadItem.url}\"", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false))
downloadItem.url = "" downloadItem.url = ""
} }

View file

@ -3,15 +3,20 @@ package com.deniscerri.ytdl.ui.downloadcard
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@ -21,7 +26,11 @@ import android.widget.Button
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts 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.get
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager 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.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.services.ProcessDownloadsInBackgroundService
import com.deniscerri.ytdl.ui.adapter.ConfigureMultipleDownloadsAdapter import com.deniscerri.ytdl.ui.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.FileUtil 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.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -75,6 +90,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var currentDownloadIDs: List<Long> private lateinit var currentDownloadIDs: List<Long>
private var processingDownloadsJobID: Long = 0
private var processingItemsCount : Int = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -86,6 +103,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
infoUtil = InfoUtil(requireContext()) infoUtil = InfoUtil(requireContext())
currentDownloadIDs = arguments?.getLongArray("currentDownloadIDs")?.toList() ?: listOf() currentDownloadIDs = arguments?.getLongArray("currentDownloadIDs")?.toList() ?: listOf()
processingDownloadsJobID = arguments?.getLong("processingDownloadsJobID") ?: 0
processingItemsCount = currentDownloadIDs.size
} }
@SuppressLint("RestrictedApi", "NotifyDataSetChanged") @SuppressLint("RestrictedApi", "NotifyDataSetChanged")
@ -131,40 +150,83 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button) val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
val download = view.findViewById<Button>(R.id.bottomsheet_download_button) val download = view.findViewById<Button>(R.id.bottomsheet_download_button)
val progressBar = view.findViewById<LinearProgressIndicator>(R.id.loadingItemsProgress)
filesize = view.findViewById(R.id.filesize) filesize = view.findViewById(R.id.filesize)
count = view.findViewById(R.id.count) count = view.findViewById(R.id.count)
scheduleBtn.setOnClickListener{ scheduleBtn.setOnClickListener{
UiUtil.showDatePicker(parentFragmentManager) { cal -> fun initSchedule() {
scheduleBtn.isEnabled = false UiUtil.showDatePicker(parentFragmentManager) { cal ->
download.isEnabled = false scheduleBtn.isEnabled = false
download.isEnabled = false
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
}
lifecycleScope.launch { val jobData = withContext(Dispatchers.IO){
withContext(Dispatchers.IO){ downloadViewModel.getLoadingProcessingDownloadJob(processingDownloadsJobID)
downloadViewModel.deleteAllWithID(currentDownloadIDs) }
downloadViewModel.downloadProcessingDownloads(cal.timeInMillis) 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 { download!!.setOnClickListener {
scheduleBtn.isEnabled = false fun initDownload() {
download.isEnabled = false scheduleBtn.isEnabled = false
lifecycleScope.launch { download.isEnabled = false
withContext(Dispatchers.IO){ lifecycleScope.launch {
downloadViewModel.deleteAllWithID(currentDownloadIDs) withContext(Dispatchers.IO){
downloadViewModel.downloadProcessingDownloads() 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 { download.setOnLongClickListener {
@ -206,8 +268,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.continueUpdatingFormatsOnBackground() downloadViewModel.continueUpdatingFormatsOnBackground()
} }
dismiss()
} }
dismiss()
} }
} }
@ -537,45 +599,55 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.processingDownloads.collectLatest { downloadViewModel.loadingProcessingDownloadsJobs.collectLatest { jobs ->
count.text = "${it.size} ${getString(R.string.selected)}" processingItemsCount = jobs.firstOrNull { j -> j.jobID == processingDownloadsJobID }?.itemIDs?.size ?: currentDownloadIDs.size
listAdapter.submitList(it) lifecycleScope.launch {
withContext(Dispatchers.Main){ downloadViewModel.processingDownloads.collectLatest {
updateFileSize(it.map { it2 -> it2.format.filesize }) count.text = "${it.size} ${getString(R.string.selected)}"
}
if (it.isNotEmpty()){ val loading = it.size != processingItemsCount
if (it.all { it2 -> it2.type == it[0].type }) { progressBar.isVisible = loading
withContext(Dispatchers.Main) { bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading }
bottomAppBar.menu[1].icon?.alpha = 255
if (it[0].type != DownloadViewModel.Type.command) { listAdapter.submitList(it)
bottomAppBar.menu[3].icon?.alpha = 255 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){ UiUtil.showGenericDeleteDialog(requireContext(), deletedItem.title){
lifecycleScope.launch { lifecycleScope.launch {
val count = withContext(Dispatchers.IO){ processingItemsCount--
downloadViewModel.getProcessingDownloadsCount()
}
downloadViewModel.deleteDownload(id) 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) Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) { .setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
processingItemsCount++
downloadViewModel.insert(deletedItem) downloadViewModel.insert(deletedItem)
} }
}.show() }.show()
@ -736,9 +807,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {
requireActivity().lifecycleScope.launch { downloadViewModel.cancelLoadingProcessingDownloads(processingDownloadsJobID)
downloadViewModel.deleteProcessing() downloadViewModel.deleteProcessing()
}
super.onDismiss(dialog) super.onDismiss(dialog)
} }
@ -757,16 +827,16 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val deletedItem = withContext(Dispatchers.IO){ val deletedItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID) downloadViewModel.getItemByID(itemID)
} }
val count = withContext(Dispatchers.IO){ processingItemsCount--
downloadViewModel.getProcessingDownloadsCount()
}
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.deleteDownload(deletedItem.id) 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) Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) { .setAction(getString(R.string.undo)) {
processingItemsCount++
downloadViewModel.insert(deletedItem) downloadViewModel.insert(deletedItem)
}.show() }.show()
}else{ }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 } .sumOf { itt -> itt.filesize }
} }
} }
hasGenericFormats = false hasGenericFormats = formatCollection.size == 0
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
shimmers.visibility = View.GONE shimmers.visibility = View.GONE
shimmers.stopShimmer() shimmers.stopShimmer()
filterBtn.isVisible = true filterBtn.isVisible = !hasGenericFormats
addFormatsToView() addFormatsToView()
refreshBtn.visibility = View.GONE refreshBtn.isVisible = hasGenericFormats
refreshBtn.isEnabled = hasGenericFormats
formatListLinearLayout.visibility = View.VISIBLE formatListLinearLayout.visibility = View.VISIBLE
} }
}catch (e: Exception){ }catch (e: Exception){

View file

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

View file

@ -11,9 +11,12 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.button.MaterialButton
import com.google.android.material.elevation.SurfaceColors import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -49,28 +53,23 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
private lateinit var count: TextView private lateinit var count: TextView
private lateinit var selectBetween: MenuItem 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 itemURLs: List<String>
private lateinit var type: DownloadViewModel.Type private var items = listOf<ResultItem>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java] resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
if (Build.VERSION.SDK_INT >= 33){ arguments?.getLongArray("resultIDs").apply {
arguments?.getParcelableArrayList("results", ResultItem::class.java)
}else{
arguments?.getParcelableArrayList<ResultItem>("results")
}.apply {
if (this == null){ if (this == null){
dismiss() dismiss()
return return
}else{ }else{
items = this resultItemIDs = this.toList()
} }
} }
type = arguments?.getSerializable("type") as DownloadViewModel.Type
} }
@SuppressLint("RestrictedApi", "NotifyDataSetChanged") @SuppressLint("RestrictedApi", "NotifyDataSetChanged")
@ -90,6 +89,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
} }
} }
val progress = view.findViewById<LinearProgressIndicator>(R.id.loadingItemsProgress)
listAdapter = listAdapter =
PlaylistAdapter( PlaylistAdapter(
@ -101,7 +101,6 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = listAdapter recyclerView.adapter = listAdapter
recyclerView.enableFastScroll() recyclerView.enableFastScroll()
listAdapter.submitList(items)
count = view.findViewById(R.id.count) count = view.findViewById(R.id.count)
count.text = "0 ${resources.getString(R.string.selected)}" count.text = "0 ${resources.getString(R.string.selected)}"
@ -159,7 +158,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
fromTextInput.isEnabled = true fromTextInput.isEnabled = true
toTextInput.isEnabled = true toTextInput.isEnabled = true
ok.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{ }else{
reset() reset()
fromTextInput.isEnabled = true fromTextInput.isEnabled = true
@ -183,25 +182,15 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf( findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf(
Pair("result", resultItem), Pair("result", resultItem),
Pair("type", downloadViewModel.getDownloadType(type, resultItem.url)), Pair("type", downloadViewModel.getDownloadType(url = resultItem.url)),
)) ))
} }
}else{ }else{
val downloadItems = mutableListOf<DownloadItem>() val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(checkedResultItems.map { it!!.id })
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)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadMultipleBottomSheetDialog) findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadMultipleBottomSheetDialog,
bundleOf(Pair("processingDownloadsJobID", jobID))
)
} }
} }
@ -218,7 +207,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
listAdapter.invertSelected(items) listAdapter.invertSelected(items)
val checkedItems = listAdapter.getCheckedItems() val checkedItems = listAdapter.getCheckedItems()
if (checkedItems.size == items.size){ 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{ }else{
count.text = "${checkedItems.size} ${resources.getString(R.string.selected)}" 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) 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>) { override fun onCardSelect(itemURL: String, isChecked: Boolean, checkedItems: List<String>) {
if (checkedItems.size == items.size){ 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{ }else{
count.text = "${checkedItems.size} ${resources.getString(R.string.selected)}" 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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -251,19 +253,18 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
R.id.redownload -> { R.id.redownload -> {
lifecycleScope.launch { lifecycleScope.launch {
val selectedObjects = getSelectedIDs() val selectedObjects = getSelectedIDs()
if (preferences.getBoolean("download_card", true)){ val showDownloadCard = preferences.getBoolean("download_card", true)
withContext(Dispatchers.IO){ if (showDownloadCard) {
downloadViewModel.addDownloadsToProcessing(selectedObjects) CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
downloadViewModel.turnDownloadItemsToProcessingDownloads(selectedObjects)
} }
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
val bundle = Bundle() val bundle = Bundle()
bundle.putLongArray("currentDownloadIDs", selectedObjects.toLongArray()) bundle.putLongArray("currentDownloadIDs", selectedObjects.toLongArray())
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2,bundle) findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundle)
}
}else{
withContext(Dispatchers.IO) {
downloadViewModel.reQueueDownloadItems(selectedObjects)
} }
}else {
downloadViewModel.reQueueDownloadItems(selectedObjects)
} }
adapter.clearCheckedItems() adapter.clearCheckedItems()
@ -275,7 +276,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
} }
R.id.select_all -> { R.id.select_all -> {
adapter.checkAll() adapter.checkAll()
mode?.title = getString(R.string.all_items_selected) mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
true true
} }
R.id.invert_selected -> { R.id.invert_selected -> {

View file

@ -1,8 +1,13 @@
package com.deniscerri.ytdl.ui.downloads package com.deniscerri.ytdl.ui.downloads
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -10,9 +15,11 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
@ -22,6 +29,7 @@ import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.services.ProcessDownloadsInBackgroundService
import com.deniscerri.ytdl.util.Extensions.createBadge import com.deniscerri.ytdl.util.Extensions.createBadge
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
@ -116,9 +124,24 @@ class DownloadQueueMainFragment : Fragment(){
initMenu() initMenu()
if (arguments?.getString("tab") != null){ if (arguments?.getString("tab") != null){
tabLayout.getTabAt(3)!!.select() tabLayout.getTabAt(4)!!.select()
viewPager2.postDelayed( { viewPager2.postDelayed( {
viewPager2.setCurrentItem(3, false) viewPager2.setCurrentItem(4, false)
val reconfigureID = arguments?.getLong("reconfigure")
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) }, 200)
} }
@ -243,14 +266,37 @@ class DownloadQueueMainFragment : Fragment(){
workManager.cancelAllWorkByTag("download") workManager.cancelAllWorkByTag("download")
lifecycleScope.launch { lifecycleScope.launch {
val notificationUtil = NotificationUtil(requireContext()) val notificationUtil = NotificationUtil(requireContext())
downloadViewModel.cancelActiveQueued()
cancelBackgroundProcessingDownloads()
val activeAndQueued = withContext(Dispatchers.IO){ val activeAndQueued = withContext(Dispatchers.IO){
downloadViewModel.getActiveAndQueuedDownloadIDs() downloadViewModel.getActiveAndQueuedDownloadIDs()
} }
downloadViewModel.cancelActiveQueued()
activeAndQueued.forEach { id -> activeAndQueued.forEach { id ->
YoutubeDL.getInstance().destroyProcessById(id.toString()) YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id.toInt()) 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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -243,20 +245,18 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
R.id.redownload -> { R.id.redownload -> {
lifecycleScope.launch { lifecycleScope.launch {
val selectedObjects = getSelectedIDs() val selectedObjects = getSelectedIDs()
val showDownloadCard = preferences.getBoolean("download_card", true)
if (preferences.getBoolean("download_card", true)){ if (showDownloadCard) {
withContext(Dispatchers.IO){ CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
downloadViewModel.addDownloadsToProcessing(selectedObjects) downloadViewModel.turnDownloadItemsToProcessingDownloads(selectedObjects)
} }
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
val bundle = Bundle() val bundle = Bundle()
bundle.putLongArray("currentDownloadIDs", selectedObjects.toLongArray()) bundle.putLongArray("currentDownloadIDs", selectedObjects.toLongArray())
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2,bundle) findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundle)
}
}else{
withContext(Dispatchers.IO) {
downloadViewModel.reQueueDownloadItems(selectedObjects)
} }
}else {
downloadViewModel.reQueueDownloadItems(selectedObjects)
} }
adapter.clearCheckedItems() adapter.clearCheckedItems()
@ -266,7 +266,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
} }
R.id.select_all -> { R.id.select_all -> {
adapter.checkAll() adapter.checkAll()
mode?.title = getString(R.string.all_items_selected) mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
true true
} }
R.id.invert_selected -> { R.id.invert_selected -> {

View file

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

View file

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

View file

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

View file

@ -351,7 +351,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
templatesAdapter.checkAll(templatesList) templatesAdapter.checkAll(templatesList)
selectedObjects.clear() selectedObjects.clear()
templatesList.forEach { selectedObjects.add(it) } 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 true
} }
R.id.invert_selected -> { R.id.invert_selected -> {

View file

@ -193,7 +193,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
downloadLogAdapter.checkAll(items) downloadLogAdapter.checkAll(items)
selectedObjects.clear() selectedObjects.clear()
items.forEach { selectedObjects.add(it) } 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 true
} }
R.id.invert_selected -> { R.id.invert_selected -> {

View file

@ -1,8 +1,11 @@
package com.deniscerri.ytdl.ui.more.settings package com.deniscerri.ytdl.ui.more.settings
import android.content.DialogInterface import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
@ -13,11 +16,10 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.DownloadWorker 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.Calendar
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -25,6 +27,8 @@ import java.util.concurrent.TimeUnit
class DownloadSettingsFragment : BaseSettingsFragment() { class DownloadSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.downloads override val title: Int = R.string.downloads
private lateinit var archivePath: Preference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.downloading_preferences, rootKey) setPreferencesFromResource(R.xml.downloading_preferences, rootKey)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
@ -47,6 +51,25 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
true 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 useScheduler = findPreference<SwitchPreferenceCompat>("use_scheduler")
val scheduleStart = findPreference<Preference>("schedule_start") val scheduleStart = findPreference<Preference>("schedule_start")
scheduleStart?.summary = preferences.getString("schedule_start", "00:00") scheduleStart?.summary = preferences.getString("schedule_start", "00:00")
@ -55,8 +78,17 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
val scheduler = AlarmScheduler(requireContext()) val scheduler = AlarmScheduler(requireContext())
useScheduler?.setOnPreferenceChangeListener { preference, newValue -> useScheduler?.setOnPreferenceChangeListener { preference, newValue ->
var allowChange = true
if (newValue as Boolean){ 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{ }else{
scheduler.cancel() scheduler.cancel()
//start worker if there are leftover downloads waiting for scheduler //start worker if there are leftover downloads waiting for scheduler
@ -72,7 +104,7 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
workRequest.build() workRequest.build()
) )
} }
true allowChange
} }
scheduleStart?.setOnPreferenceClickListener { 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 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.os.Bundle
import android.widget.LinearLayout
import android.widget.TextView
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceManager 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.R
import com.deniscerri.ytdl.ui.adapter.SortableTextItemAdapter
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class ProcessingSettingsFragment : BaseSettingsFragment() { class ProcessingSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.processing override val title: Int = R.string.processing
@SuppressLint("RestrictedApi")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.processing_preferences, rootKey) setPreferencesFromResource(R.xml.processing_preferences, rootKey)
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity())
val editor = prefs.edit() val editor = prefs.edit()
val preferredFormatID : EditTextPreference? = findPreference("format_id") val preferredFormatID : EditTextPreference? = findPreference("format_id")
val preferredFormatIDAudio : EditTextPreference? = findPreference("format_id_audio") val preferredFormatIDAudio : EditTextPreference? = findPreference("format_id_audio")
val subtitleLanguages : Preference? = findPreference("subs_lang") 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?.title = "${getString(R.string.preferred_format_id)} [${getString(R.string.video)}]"
preferredFormatID?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]" preferredFormatID?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
@ -34,6 +48,119 @@ class ProcessingSettingsFragment : BaseSettingsFragment() {
} }
true 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 package com.deniscerri.ytdl.util
import android.R.color
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
@ -37,9 +36,11 @@ import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.App import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory
import com.deniscerri.ytdl.util.Extensions.toTimePeriodsArray
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
@ -56,6 +57,7 @@ import java.util.Calendar
import java.util.Locale import java.util.Locale
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min
object Extensions { 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 { return try {
val timeArray = this.split(":") 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 var times = 60
for (i in timeArray.lastIndex - 1 downTo 0) { for (i in timeArray.lastIndex - 1 downTo 0) {
timeSeconds += timeArray[i].toInt() * times timeSeconds += timeArray[i].toInt() * times
times *= 60 times *= 60
} }
timeSeconds
(timeSeconds * 1000) + millis * 100
}catch (e: Exception){ }catch (e: Exception){
e.printStackTrace() e.printStackTrace()
0 0L
} }
} }
@ -408,11 +457,15 @@ object Extensions {
paint.setColorFilter(PorterDuffColorFilter(colorValue.data, PorterDuff.Mode.SRC_IN)) paint.setColorFilter(PorterDuffColorFilter(colorValue.data, PorterDuff.Mode.SRC_IN))
val canvas = Canvas(bitmap) 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.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas) drawable.draw(canvas)
return bitmap 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 android.webkit.MimeTypeMap
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile 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.FileCallback
import com.anggrayudi.storage.callback.FolderCallback import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.copyFolderTo import com.anggrayudi.storage.file.copyFolderTo
@ -294,7 +296,8 @@ object FileUtil {
} }
fun getDownloadArchivePath(context: Context) : String { 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 { 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.getIntByAny
import com.deniscerri.ytdl.util.Extensions.getStringByAny import com.deniscerri.ytdl.util.Extensions.getStringByAny
import com.deniscerri.ytdl.util.Extensions.toStringDuration import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.work.TerminalDownloadWorker
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
@ -488,6 +489,7 @@ class InfoUtil(private val context: Context) {
if (proxy!!.isNotBlank()) { if (proxy!!.isNotBlank()) {
request.addOption("--proxy", proxy) request.addOption("--proxy", proxy)
} }
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
@ -546,7 +548,7 @@ class InfoUtil(private val context: Context) {
if (proxy!!.isNotBlank()){ if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy) request.addOption("--proxy", proxy)
} }
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
YoutubeDL.getInstance().execute(request){ progress, _, line -> YoutubeDL.getInstance().execute(request){ progress, _, line ->
@ -631,6 +633,7 @@ class InfoUtil(private val context: Context) {
if (proxy!!.isNotBlank()){ if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy) request.addOption("--proxy", proxy)
} }
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
@ -904,6 +907,7 @@ class InfoUtil(private val context: Context) {
if (proxy!!.isNotBlank()){ if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy) 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()){ 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()){ 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)$") 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) val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
if (embedThumb) { if (embedThumb) {
request.addOption("--embed-thumbnail") 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()){ if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters") 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{ }else{
if (filenameTemplate.isNotBlank()){ if (filenameTemplate.isNotBlank()){
request.addOption("-o", "${filenameTemplate.removeSuffix(".%(ext)s")}.%(ext)s") 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.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)) } audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",resources.getString(R.string.preferred_format_id), "",1, it)) }
return formats return formats
} }
@ -1520,7 +1524,7 @@ class InfoUtil(private val context: Context) {
videoCodecs[videoCodecsValues.indexOf(this)] 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)) } formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.preferred_format_id),"", "",1, it)) }
return formats return formats
} }

View file

@ -15,6 +15,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.os.bundleOf
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.navigation.NavDeepLinkBuilder import androidx.navigation.NavDeepLinkBuilder
import com.deniscerri.ytdl.R 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 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 finishedDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_FINISHED_CHANNEL_ID)
private val erroredDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_ERRORED_CHANNEL_ID) private val erroredDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_ERRORED_CHANNEL_ID)
private val miscDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_MISC_CHANNEL_ID)
private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java) private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java)
private val resources: Resources = context.resources private val resources: Resources = context.resources
@ -99,6 +101,7 @@ class NotificationUtil(var context: Context) {
DOWNLOAD_FINISHED_CHANNEL_ID -> { return finishedDownloadNotificationBuilder } DOWNLOAD_FINISHED_CHANNEL_ID -> { return finishedDownloadNotificationBuilder }
DOWNLOAD_WORKER_CHANNEL_ID -> { return workerNotificationBuilder } DOWNLOAD_WORKER_CHANNEL_ID -> { return workerNotificationBuilder }
DOWNLOAD_ERRORED_CHANNEL_ID -> { return erroredDownloadNotificationBuilder } DOWNLOAD_ERRORED_CHANNEL_ID -> { return erroredDownloadNotificationBuilder }
DOWNLOAD_MISC_CHANNEL_ID -> { return miscDownloadNotificationBuilder }
} }
return downloadNotificationBuilder return downloadNotificationBuilder
} }
@ -336,13 +339,21 @@ class NotificationUtil(var context: Context) {
.setArguments(bundle) .setArguments(bundle)
.createPendingIntent() .createPendingIntent()
val tabBundle = Bundle()
tabBundle.putString("tab", "error")
val errorTabPendingIntent = NavDeepLinkBuilder(context) val errorTabPendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph) .setGraph(R.navigation.nav_graph)
.setDestination(R.id.downloadQueueMainFragment) .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() .createPendingIntent()
notificationBuilder notificationBuilder
@ -359,6 +370,8 @@ class NotificationUtil(var context: Context) {
.setPriority(NotificationCompat.PRIORITY_MAX) .setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions() .clearActions()
notificationBuilder.addAction(0, res.getString(R.string.configure_download), reconfigurePendingItent)
if (logID != null){ if (logID != null){
notificationBuilder.addAction(0, res.getString(R.string.logs), errorPendingIntent) 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()) 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 { companion object {
const val DOWNLOAD_SERVICE_CHANNEL_ID = "1" const val DOWNLOAD_SERVICE_CHANNEL_ID = "1"
const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2" const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2"

View file

@ -617,7 +617,7 @@ object UiUtil {
val command = bottomSheet.findViewById<Chip>(R.id.command) val command = bottomSheet.findViewById<Chip>(R.id.command)
when(status){ when(status){
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused -> { DownloadRepository.Status.Scheduled -> {
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) time!!.visibility = View.GONE if (item.downloadStartTime <= System.currentTimeMillis() / 1000) time!!.visibility = View.GONE
else { else {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
@ -712,7 +712,8 @@ object UiUtil {
download!!.visibility = View.GONE download!!.visibility = View.GONE
} }
DownloadRepository.Status.Scheduled -> { 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 -> { else -> {
download?.setOnLongClickListener { download?.setOnLongClickListener {
@ -813,6 +814,7 @@ object UiUtil {
val codec = bottomSheet.findViewById<TextView>(R.id.codec) val codec = bottomSheet.findViewById<TextView>(R.id.codec)
val fileSize = bottomSheet.findViewById<TextView>(R.id.file_size) val fileSize = bottomSheet.findViewById<TextView>(R.id.file_size)
val command = bottomSheet.findViewById<Chip>(R.id.command) val command = bottomSheet.findViewById<Chip>(R.id.command)
val location = bottomSheet.findViewById<Chip>(R.id.location)
val file = File(item.downloadPath.first()) val file = File(item.downloadPath.first())
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
@ -858,6 +860,11 @@ object UiUtil {
showGeneratedCommand(context, item.command) 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 link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link)
val url = item.url val url = item.url
@ -1664,169 +1671,6 @@ object UiUtil {
errDialog.show() 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){ fun showGenericDeleteDialog(context: Context, itemTitle: String, accepted: () -> Unit){
val deleteDialog = MaterialAlertDialogBuilder(context) val deleteDialog = MaterialAlertDialogBuilder(context)
deleteDialog.setTitle(context.getString(R.string.you_are_going_to_delete) + " \"" + itemTitle + "\"!") deleteDialog.setTitle(context.getString(R.string.you_are_going_to_delete) + " \"" + itemTitle + "\"!")

View file

@ -1,18 +1,28 @@
package com.deniscerri.ytdl.work package com.deniscerri.ytdl.work
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdl.receiver.CancelScheduleAlarmReceiver
import com.deniscerri.ytdl.receiver.ScheduleAlarmReceiver
import java.util.Calendar import java.util.Calendar
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class AlarmScheduler(private val context: Context) { class AlarmScheduler(private val context: Context) {
private val preferences = PreferenceManager.getDefaultSharedPreferences(context) private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
@SuppressLint("ScheduleExactAlarm")
fun schedule() { fun schedule() {
cancel() cancel()
@ -25,17 +35,15 @@ class AlarmScheduler(private val context: Context) {
val time = calculateNextTime(sTime) val time = calculateNextTime(sTime)
//schedule starting work //schedule starting work
val workConstraints = Constraints.Builder() alarmManager?.setExactAndAllowWhileIdle(
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>() AlarmManager.RTC_WAKEUP,
.addTag("scheduledDownload") time.timeInMillis,
.addTag("download") PendingIntent.getBroadcast(
.setConstraints(workConstraints.build()) context,
.setInitialDelay(System.currentTimeMillis() - time.timeInMillis, TimeUnit.MILLISECONDS) 0,
Intent(context, ScheduleAlarmReceiver::class.java),
WorkManager.getInstance(context).enqueueUniqueWork( PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
System.currentTimeMillis().toString(), )
ExistingWorkPolicy.REPLACE,
workRequest.build()
) )
//schedule closing work //schedule closing work
@ -46,27 +54,35 @@ class AlarmScheduler(private val context: Context) {
sTime.set(Calendar.SECOND, 0) sTime.set(Calendar.SECOND, 0)
val calendar = calculateNextTime(eTime) val calendar = calculateNextTime(eTime)
alarmManager?.setExactAndAllowWhileIdle(
val workRequest2 = OneTimeWorkRequestBuilder<CancelScheduledDownloadWorker>() AlarmManager.RTC_WAKEUP,
.addTag("cancelScheduledDownload") calendar.timeInMillis,
.setConstraints(workConstraints.build()) PendingIntent.getBroadcast(
.setInitialDelay(System.currentTimeMillis() - calendar.timeInMillis + 60000, TimeUnit.MILLISECONDS) context,
0,
WorkManager.getInstance(context).enqueueUniqueWork( Intent(context, CancelScheduleAlarmReceiver::class.java),
System.currentTimeMillis().toString(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
ExistingWorkPolicy.REPLACE, )
workRequest2.build()
) )
} }
fun cancel() { fun cancel() {
val wm = WorkManager.getInstance(context) val intent = Intent(context, ScheduleAlarmReceiver::class.java)
wm.cancelAllWorkByTag("scheduledDownload") val pendingIntent = PendingIntent.getService(context, 0, intent,
wm.cancelAllWorkByTag("cancelScheduledDownload") 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() val calendar = Calendar.getInstance()
if (c.get(Calendar.HOUR_OF_DAY) < calendar.get(Calendar.HOUR_OF_DAY)){ if (c.get(Calendar.HOUR_OF_DAY) < calendar.get(Calendar.HOUR_OF_DAY)){
c.add(Calendar.DATE, 1) c.add(Calendar.DATE, 1)
@ -110,4 +126,12 @@ class AlarmScheduler(private val context: Context) {
return false 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") WorkManager.getInstance(context).cancelAllWorkByTag("download")
runningDownloads.forEach { runningDownloads.forEach {
YoutubeDL.getInstance().destroyProcessById(it.id.toString()) YoutubeDL.getInstance().destroyProcessById(it.id.toString())
it.status = DownloadRepository.Status.Queued.toString()
dao.update(it)
} }
return Result.success() return Result.success()
} }
companion object {
val runningYTDLInstances: MutableList<Long> = mutableListOf()
const val TAG = "DownloadWorker"
}
} }

View file

@ -119,7 +119,10 @@ class DownloadWorker(
delay(1500) delay(1500)
//update item if its incomplete //update item if its incomplete
resultRepo.updateDownloadItem(downloadItem)?.apply { 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 { }.onSuccess {
resultRepo.updateDownloadItem(downloadItem)?.apply { resultRepo.updateDownloadItem(downloadItem)
dao.updateWithoutUpsert(this)
}
val wasQuickDownloaded = resultDao.getCountInt() == 0 val wasQuickDownloaded = resultDao.getCountInt() == 0
runBlocking { runBlocking {
var finalPaths : MutableList<String>? var finalPaths : MutableList<String>?
@ -241,9 +242,9 @@ class DownloadWorker(
val incognito = sharedPreferences.getBoolean("incognito", false) val incognito = sharedPreferences.getBoolean("incognito", false)
if (!incognito) { if (!incognito) {
if (request.hasOption("--download-archive") && finalPaths == listOf(context.getString(R.string.unfound_file))) { 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() Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
} }, 100)
}else{ }else{
val unixTime = System.currentTimeMillis() / 1000 val unixTime = System.currentTimeMillis() / 1000
finalPaths?.apply { finalPaths?.apply {
@ -303,6 +304,8 @@ class DownloadWorker(
} }
}.onFailure { }.onFailure {
if (this@DownloadWorker.isStopped) return@onFailure
FileUtil.deleteConfigFiles(request) FileUtil.deleteConfigFiles(request)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
@ -345,7 +348,6 @@ class DownloadWorker(
setProgressAsync(workDataOf("progress" to 100, "output" to it.toString(), "id" to downloadItem.id)) 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.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
@ -205,7 +206,14 @@ class ObserveSourceWorker(
} }
//schedule for next time //schedule for next time
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workConstraints = Constraints.Builder() val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>() val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
.addTag("observeSources") .addTag("observeSources")
.addTag(sourceID.toString()) .addTag(sourceID.toString())

View file

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

View file

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

View file

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

View file

@ -253,56 +253,50 @@
</com.facebook.shimmer.ShimmerFrameLayout> </com.facebook.shimmer.ShimmerFrameLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout <LinearLayout
android:id="@+id/home_fabs" android:id="@+id/home_fabs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" > android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="bottom|end">
<LinearLayout <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:layout_width="match_parent" android:id="@+id/copied_url_fab"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_gravity="bottom|end"
android:paddingVertical="10dp" android:text="@string/link_you_copied"
android:layout_gravity="bottom|end"> android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied"
app:icon="@drawable/ic_clipboard"
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/copied_url_fab" android:id="@+id/download_all_fab"
android:visibility="gone" android:visibility="gone"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:text="@string/link_you_copied" android:layout_marginBottom="10dp"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:contentDescription="@string/link_you_copied" android:text="@string/download_all"
app:icon="@drawable/ic_clipboard" app:icon="@drawable/ic_down" />
app:useCompatPadding="true" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/download_all_fab" android:id="@+id/download_selected_fab"
android:visibility="gone" android:visibility="gone"
android:layout_width="wrap_content" app:elevation="0dp"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:layout_gravity="bottom|end" android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp" android:layout_gravity="bottom|end"
android:layout_marginTop="10dp" android:layout_margin="16dp"
android:text="@string/download_all" android:layout_marginBottom="10dp"
app:icon="@drawable/ic_down" /> app:icon="@drawable/ic_down"
android:text="@string/download"
app:srcCompat="@drawable/ic_music"/>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton </LinearLayout>
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>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </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_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="3" android:maxLines="2"
app:layout_constraintVertical_bias="0.0"
android:paddingHorizontal="5dp" android:paddingHorizontal="5dp"
android:scrollbars="none" android:scrollbars="none"
android:textSize="15sp" android:textSize="15sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/horizontalScrollView" app:layout_constraintEnd_toStartOf="@+id/options"
app:layout_constraintEnd_toStartOf="@+id/already_exists_buttons"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -92,10 +90,9 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:barrierDirection="bottom" app:barrierDirection="bottom"
app:constraint_referenced_ids="title,already_exists_buttons" /> app:constraint_referenced_ids="title,options" />
<HorizontalScrollView <HorizontalScrollView
android:id="@+id/horizontalScrollView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="bottom" android:gravity="bottom"
@ -112,6 +109,44 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent"> 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 <TextView
android:id="@+id/format_note" android:id="@+id/format_note"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary" style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
@ -153,24 +188,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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> </LinearLayout>
@ -178,39 +196,25 @@
</HorizontalScrollView> </HorizontalScrollView>
<LinearLayout <com.google.android.material.button.MaterialButton
android:id="@+id/already_exists_buttons" android:id="@+id/options"
android:layout_width="wrap_content" style="?attr/materialIconButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content" 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_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"> 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>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</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_constraintBottom_toBottomOf="@+id/frame_layout"
app:layout_constraintEnd_toEndOf="@+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 <TextView
android:id="@+id/durationText" android:id="@+id/durationText"
@ -162,6 +179,7 @@
android:hint="@string/start"> android:hint="@string/start">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/from_textinput_edittext"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="text" /> android:inputType="text" />
@ -189,6 +207,7 @@
app:layout_constraintStart_toEndOf="@+id/colon"> app:layout_constraintStart_toEndOf="@+id/colon">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/to_textinput_edittext"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="text" /> android:inputType="text" />

View file

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

View file

@ -17,91 +17,64 @@
android:id="@+id/scr" android:id="@+id/scr"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingTop="10dp"
android:scrollbars="none" android:scrollbars="none"
android:paddingTop="20dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<LinearLayout <com.google.android.material.card.MaterialCardView
android:id="@+id/current"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp" android:orientation="vertical"
android:orientation="horizontal" android:padding="10dp">
android:paddingTop="20dp">
<TextView <TextView
android:id="@+id/bottom_sheet_title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="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" android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout>
<com.google.android.material.card.MaterialCardView </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>
</ScrollView> </ScrollView>
<com.google.android.material.textfield.TextInputLayout <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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp" android:layout_marginHorizontal="20dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:contentDescription="@string/extra_command" android:hint="@string/extra_command"
app:layout_constraintTop_toBottomOf="@+id/scr"> app:layout_constraintTop_toBottomOf="@+id/scr">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/command" android:id="@+id/command"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textSize="14sp" android:textSize="14sp"
android:inputType="textMultiLine" android:inputType="textMultiLine"
android:maxLines="2000" /> android:maxLines="2000" />

View file

@ -13,6 +13,8 @@
android:id="@+id/file_name" android:id="@+id/file_name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -37,7 +39,7 @@
android:scrollbars="none" android:scrollbars="none"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
@ -114,19 +116,6 @@
</HorizontalScrollView> </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>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -18,7 +18,7 @@
android:scrollbars="none" android:scrollbars="none"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
android:paddingBottom="100dp" android:layout_marginBottom="100dp"
app:layout_constraintTop_toBottomOf="@+id/pause_resume"> app:layout_constraintTop_toBottomOf="@+id/pause_resume">
</androidx.recyclerview.widget.RecyclerView> </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:id="@+id/container_textview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textDirection="locale"
android:inputType="none" android:inputType="none"
app:simpleItems="@array/audio_containers" app:simpleItems="@array/audio_containers"
/> />

View file

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

View file

@ -155,6 +155,21 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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> </com.google.android.material.chip.ChipGroup>
</HorizontalScrollView> </HorizontalScrollView>

View file

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

View file

@ -129,6 +129,13 @@
</androidx.constraintlayout.widget.ConstraintLayout> </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 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloadMultipleRecyclerview" android:id="@+id/downloadMultipleRecyclerview"
android:layout_width="match_parent" 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 <action
android:id="@+id/action_homeFragment_to_downloadMultipleBottomSheetDialog2" android:id="@+id/action_homeFragment_to_downloadMultipleBottomSheetDialog2"
app:destination="@id/downloadMultipleBottomSheetDialog2" /> app:destination="@id/downloadMultipleBottomSheetDialog2" />
<action
android:id="@+id/action_homeFragment_to_downloadsAlreadyExistDialog"
app:destination="@id/downloadsAlreadyExistDialog" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/historyFragment" android:id="@+id/historyFragment"
@ -42,6 +45,9 @@
<action <action
android:id="@+id/action_historyFragment_to_downloadQueueMainFragment" android:id="@+id/action_historyFragment_to_downloadQueueMainFragment"
app:destination="@id/downloadQueueMainFragment" /> app:destination="@id/downloadQueueMainFragment" />
<action
android:id="@+id/action_historyFragment_to_downloadsAlreadyExistDialog"
app:destination="@id/downloadsAlreadyExistDialog" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/moreFragment" android:id="@+id/moreFragment"
@ -191,4 +197,8 @@
android:id="@+id/observeSourcesBottomSheetDialog" android:id="@+id/observeSourcesBottomSheetDialog"
android:name="com.deniscerri.ytdl.ui.downloadcard.ObserveSourcesBottomSheetDialog" android:name="com.deniscerri.ytdl.ui.downloadcard.ObserveSourcesBottomSheetDialog"
android:label="ObserveSourcesBottomSheetDialog" /> android:label="ObserveSourcesBottomSheetDialog" />
<dialog
android:id="@+id/downloadsAlreadyExistDialog"
android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadsAlreadyExistDialog"
android:label="DownloadsAlreadyExistDialog" />
</navigation> </navigation>

View file

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

View file

@ -40,37 +40,37 @@
</string-array> </string-array>
<string-array name="video_formats"> <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>@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>
<string-array name="video_formats_values"> <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>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>
<string-array name="audio_formats"> <string-array name="audio_formats">
<item>worst</item>
<item>best</item> <item>best</item>
<item>worst</item>
</string-array> </string-array>
<string-array name="audio_formats_values"> <string-array name="audio_formats_values">
<item>wa</item>
<item>ba</item> <item>ba</item>
<item>wa</item>
</string-array> </string-array>
<string-array name="format_ordering"> <string-array name="format_ordering">
@ -896,6 +896,7 @@
<item>%(uploader)s___(string): Full name of the video uploader)</item> <item>%(uploader)s___(string): Full name of the video uploader)</item>
<item>%(title)s___(string): Video title)</item> <item>%(title)s___(string): Video title)</item>
<item>%(id)s___(string): Video identifier)</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>%(fulltitle)s___(string): Video title ignoring live timestamp and generic title)</item>
<!-- <item>%(ext)s___(string): Video filename extension)</item>--> <!-- <item>%(ext)s___(string): Video filename extension)</item>-->
<item>%(alt_title)s___(string): A secondary title of the video)</item> <item>%(alt_title)s___(string): A secondary title of the video)</item>
@ -1418,4 +1419,34 @@
<item>config</item> <item>config</item>
</string-array> </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> </resources>

View file

@ -391,4 +391,15 @@
<string name="move_bottom">Move to the Bottom</string> <string name="move_bottom">Move to the Bottom</string>
<string name="scheduled">Scheduled</string> <string name="scheduled">Scheduled</string>
<string name="clear_scheduled">Clear 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> </resources>

View file

@ -37,6 +37,12 @@
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:title="@string/prevent_duplicate_downloads" /> 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 <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true" app:defaultValue="true"

View file

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

View file

@ -54,6 +54,14 @@
android:summary="@string/use_extra_commands_summary" android:summary="@string/use_extra_commands_summary"
app:title="@string/use_extra_commands" /> 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>
<PreferenceCategory android:title="@string/audio"> <PreferenceCategory android:title="@string/audio">
@ -82,14 +90,6 @@
app:summary="@string/crop_thumb_summary" app:summary="@string/crop_thumb_summary"
app:title="@string/crop_thumb" /> 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> </PreferenceCategory>
@ -144,7 +144,7 @@
app:title="@string/save_thumb" /> app:title="@string/save_thumb" />
<ListPreference <ListPreference
android:defaultValue="png" android:defaultValue="jpg"
android:entries="@array/thumbnail_containers" android:entries="@array/thumbnail_containers"
android:entryValues="@array/thumbnail_containers_values" android:entryValues="@array/thumbnail_containers_values"
android:icon="@drawable/ic_image" android:icon="@drawable/ic_image"
@ -254,6 +254,17 @@
app:icon="@drawable/ic_format" app:icon="@drawable/ic_format"
app:key="prefer_smaller_formats" app:key="prefer_smaller_formats"
app:title="@string/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> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View file

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

View file

@ -14,7 +14,7 @@ buildscript {
commonsIoVer = '2.5' commonsIoVer = '2.5'
// supports java 1.6 // supports java 1.6
commonsCompressVer = '1.12' commonsCompressVer = '1.12'
youtubedlAndroidVer = "-SNAPSHOT" youtubedlAndroidVer = "ed652962e5"
youtubedlAndroidVerNoPyCrypto = "97bee6c0e8" youtubedlAndroidVerNoPyCrypto = "97bee6c0e8"
oldytdlAndroidVer = "0.14.0" oldytdlAndroidVer = "0.14.0"
workVer = "2.9.0" workVer = "2.9.0"
@ -37,12 +37,12 @@ buildscript {
} }
plugins { plugins {
id 'com.android.application' version '8.3.1' apply false id 'com.android.application' version '8.4.0' apply false
id 'com.android.library' version '8.3.1' 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.android' version '1.8.10' apply false
id "org.jetbrains.kotlin.plugin.serialization" 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 "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 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 #Wed Oct 18 18:36:39 CEST 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists