1.7.6 beta
This commit is contained in:
parent
7f0cde7774
commit
0ce383f33e
78 changed files with 3072 additions and 1437 deletions
|
|
@ -10,13 +10,10 @@ plugins {
|
|||
def properties = new Properties()
|
||||
def versionMajor = 1
|
||||
def versionMinor = 7
|
||||
def versionPatch = 5
|
||||
def versionPatch = 6
|
||||
def versionBuild = 0 // bump for dogfood builds, public betas, etc.
|
||||
def versionExt = ""
|
||||
|
||||
if (versionBuild > 0){
|
||||
versionExt = ".${versionBuild}-beta"
|
||||
}
|
||||
def isBeta = true
|
||||
def versionExt = isBeta ? ".${versionBuild}-beta" : ""
|
||||
|
||||
android {
|
||||
namespace 'com.deniscerri.ytdl'
|
||||
|
|
|
|||
|
|
@ -19,8 +19,15 @@
|
|||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<!-- for display over apps-->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<!-- alarm scheduler-->
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||
|
||||
<!-- queueing processing downloads in the background-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
|
|
@ -127,6 +134,24 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="www.facebook.com" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="fb.watch" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="instagram.com" />
|
||||
</intent-filter>
|
||||
|
|
@ -136,6 +161,15 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="www.instagram.com" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="tiktok.com" />
|
||||
</intent-filter>
|
||||
|
|
@ -181,6 +215,33 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="www.reddit.com" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="reddit.com" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="m.reddit.com" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="pinterest.com" />
|
||||
</intent-filter>
|
||||
|
|
@ -356,6 +417,8 @@
|
|||
<receiver android:name=".receiver.CancelDownloadNotificationReceiver" />
|
||||
<receiver android:name=".receiver.CancelWorkReceiver" />
|
||||
<receiver android:name=".receiver.PauseDownloadNotificationReceiver" />
|
||||
<receiver android:name=".receiver.CancelScheduleAlarmReceiver" />
|
||||
<receiver android:name=".receiver.ScheduleAlarmReceiver" />
|
||||
|
||||
<activity
|
||||
android:name=".receiver.ResumeActivity"
|
||||
|
|
@ -370,6 +433,8 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service android:name=".services.ProcessDownloadsInBackgroundService" android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import androidx.room.Upsert
|
|||
import com.deniscerri.ytdl.database.models.DownloadItem
|
||||
import com.deniscerri.ytdl.database.models.DownloadItemSimple
|
||||
import com.deniscerri.ytdl.database.models.Format
|
||||
import com.deniscerri.ytdl.database.repository.DownloadRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
|
|
@ -130,7 +131,7 @@ interface DownloadDao {
|
|||
fun getLastDownloadId(): Long
|
||||
|
||||
@Query("SELECT status FROM downloads WHERE id=:id")
|
||||
fun checkStatus(id: Long) : String
|
||||
fun checkStatus(id: Long) : DownloadRepository.Status?
|
||||
|
||||
@Query("UPDATE downloads " +
|
||||
"SET status = CASE " +
|
||||
|
|
@ -178,7 +179,7 @@ interface DownloadDao {
|
|||
@Query("DELETE FROM downloads WHERE id in (:list)")
|
||||
suspend fun deleteAllWithIDs(list: List<Long>)
|
||||
|
||||
@Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','QueuedPaused','Active','ActivePaused')")
|
||||
@Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','QueuedPaused','Active','ActivePaused', 'Scheduled')")
|
||||
suspend fun cancelActiveQueued()
|
||||
|
||||
@Query("DELETE FROM downloads WHERE status='Processing' AND id=:id")
|
||||
|
|
@ -229,9 +230,12 @@ interface DownloadDao {
|
|||
@Query("Select url from downloads where id in (:ids)")
|
||||
fun getURLsByID(ids: List<Long>) : List<String>
|
||||
|
||||
@Query("UPDATE downloads SET downloadStartTime=0 where id in (:list)")
|
||||
@Query("UPDATE downloads SET downloadStartTime=0, status='Queued' where id in (:list)")
|
||||
suspend fun resetScheduleTimeForItems(list: List<Long>)
|
||||
|
||||
@Query("UPDATE downloads SET status='Queued' where status in ('QueuedPaused', 'ActivePaused')")
|
||||
suspend fun resetPausedItems()
|
||||
|
||||
@Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)")
|
||||
suspend fun reQueueDownloadItems(list: List<Long>)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ interface ResultDao {
|
|||
return insertMultiple(items.filter { getResultByURL(it.url) == null })
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM results WHERE id IN (:ids)")
|
||||
fun getAllByIDs(ids: List<Long>) : List<ResultItem>
|
||||
|
||||
@Update(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun update(item: ResultItem)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -222,6 +222,9 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
|
|||
|
||||
val workConstraints = Constraints.Builder()
|
||||
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
else {
|
||||
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
}
|
||||
|
||||
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
|
||||
.addTag("download")
|
||||
|
|
|
|||
|
|
@ -129,6 +129,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
|||
return resultDao.getResultByURL(url)
|
||||
}
|
||||
|
||||
fun getAllByIDs(ids: List<Long>) : List<ResultItem> {
|
||||
return resultDao.getAllByIDs(ids)
|
||||
}
|
||||
|
||||
suspend fun getResultsFromSource(inputQuery: String, resetResults: Boolean, addToResults: Boolean = true, singleItem: Boolean = false) : ArrayList<ResultItem> {
|
||||
return when(getQueryType(inputQuery)){
|
||||
SourceType.YOUTUBE_VIDEO -> {
|
||||
|
|
@ -151,9 +155,9 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
|||
|
||||
}
|
||||
|
||||
fun getQueryType(inputQuery: String) : SourceType {
|
||||
private fun getQueryType(inputQuery: String) : SourceType {
|
||||
var type = SourceType.SEARCH_QUERY
|
||||
val p = Pattern.compile("(^(https?)://(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
|
||||
val p = Pattern.compile("((^(https?)://)?(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
|
||||
val m = p.matcher(inputQuery)
|
||||
if (m.find()) {
|
||||
type = SourceType.YOUTUBE_VIDEO
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ import android.app.Application
|
|||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Parcelable
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
|
|
@ -41,14 +44,18 @@ import com.deniscerri.ytdl.util.InfoUtil
|
|||
import com.deniscerri.ytdl.work.AlarmScheduler
|
||||
import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
|
|
@ -56,6 +63,7 @@ import java.util.Locale
|
|||
class DownloadViewModel(private val application: Application) : AndroidViewModel(application) {
|
||||
private val dbManager: DBManager
|
||||
val repository : DownloadRepository
|
||||
private val sharedDownloadViewModel: SharedDownloadViewModel
|
||||
private val sharedPreferences: SharedPreferences
|
||||
private val commandTemplateDao: CommandTemplateDao
|
||||
private val infoUtil : InfoUtil
|
||||
|
|
@ -78,6 +86,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
val scheduledDownloadsCount : Flow<Int>
|
||||
val pausedDownloadsCount: Flow<Int>
|
||||
|
||||
val alreadyExistsUiState: MutableStateFlow<List<SharedDownloadViewModel.AlreadyExistsIDs>>
|
||||
|
||||
private var bestVideoFormat : Format
|
||||
private var bestAudioFormat : Format
|
||||
private var defaultVideoFormats : MutableList<Format>
|
||||
|
|
@ -97,16 +107,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
private val historyRepository: HistoryRepository
|
||||
private val resultRepository: ResultRepository
|
||||
|
||||
data class AlreadyExistsUIState(
|
||||
var historyItems: MutableList<Long>,
|
||||
var downloadItems : MutableList<Long>
|
||||
)
|
||||
|
||||
val alreadyExistsUiState: MutableStateFlow<AlreadyExistsUIState> = MutableStateFlow(AlreadyExistsUIState(
|
||||
historyItems = mutableListOf(),
|
||||
downloadItems = mutableListOf()
|
||||
))
|
||||
|
||||
enum class Type {
|
||||
auto, audio, video, command
|
||||
}
|
||||
|
|
@ -121,6 +121,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
dbManager = DBManager.getInstance(application)
|
||||
dao = dbManager.downloadDao
|
||||
repository = DownloadRepository(dao)
|
||||
sharedDownloadViewModel = SharedDownloadViewModel(application)
|
||||
alreadyExistsUiState = sharedDownloadViewModel.alreadyExistsUiState
|
||||
historyRepository = HistoryRepository(dbManager.historyDao)
|
||||
resultRepository = ResultRepository(dbManager.resultDao, application)
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
|
||||
|
|
@ -168,7 +170,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
|
||||
audioContainer = sharedPreferences.getString("audio_format", "mp3")
|
||||
bestAudioFormat = if (audioFormatIDPreference.isEmpty()){
|
||||
infoUtil.getGenericAudioFormats(resources).last()
|
||||
infoUtil.getGenericAudioFormats(resources).first()
|
||||
}else{
|
||||
Format(
|
||||
audioFormatIDPreference.first().split("+").first(),
|
||||
|
|
@ -204,132 +206,24 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
return repository.getItemByID(id)
|
||||
}
|
||||
|
||||
fun getAllByIDs(ids: List<Long>) : List<DownloadItem> {
|
||||
return repository.getAllItemsByIDs(ids)
|
||||
}
|
||||
|
||||
fun getHistoryItemById(id: Long) : HistoryItem? {
|
||||
return historyRepository.getItem(id)
|
||||
}
|
||||
|
||||
fun getDownloadType(t: Type? = null, url: String) : Type {
|
||||
var type = t
|
||||
|
||||
if (type == null){
|
||||
val preferredDownloadType = sharedPreferences.getString("preferred_download_type", Type.auto.toString())
|
||||
type = if (sharedPreferences.getBoolean("remember_download_type", false)){
|
||||
Type.valueOf(sharedPreferences.getString("last_used_download_type",
|
||||
preferredDownloadType)!!)
|
||||
}else{
|
||||
Type.valueOf(preferredDownloadType!!)
|
||||
}
|
||||
}
|
||||
|
||||
return when(type){
|
||||
Type.auto -> {
|
||||
if (urlsForAudioType.any { url.contains(it) }){
|
||||
Type.audio
|
||||
}else{
|
||||
Type.video
|
||||
}
|
||||
}
|
||||
|
||||
else -> type
|
||||
}
|
||||
return sharedDownloadViewModel.getDownloadType(t, url)
|
||||
}
|
||||
|
||||
fun createDownloadItemFromResult(result: ResultItem?, url: String = "", givenType: Type) : DownloadItem {
|
||||
val resultItem = result ?: createEmptyResultItem(url)
|
||||
|
||||
val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
|
||||
val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
|
||||
val saveAutoSubs = sharedPreferences.getBoolean("write_auto_subtitles", false)
|
||||
val addChapters = sharedPreferences.getBoolean("add_chapters", false)
|
||||
val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
|
||||
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
|
||||
val cropThumb = sharedPreferences.getBoolean("crop_thumbnail", false)
|
||||
|
||||
var type = getDownloadType(givenType, resultItem.url)
|
||||
if(type == Type.command && commandTemplateDao.getTotalNumber() == 0) type = Type.video
|
||||
|
||||
val customFileNameTemplate = when(type) {
|
||||
Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)s - %(title)s")
|
||||
Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s")
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val downloadPath = when(type){
|
||||
Type.audio -> sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())
|
||||
Type.video -> sharedPreferences.getString("video_path", FileUtil.getDefaultVideoPath())
|
||||
else -> sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
|
||||
}
|
||||
|
||||
val container = when(type){
|
||||
Type.audio -> sharedPreferences.getString("audio_format", "")
|
||||
else -> sharedPreferences.getString("video_format", "")
|
||||
}
|
||||
|
||||
|
||||
val sponsorblock = sharedPreferences.getStringSet("sponsorblock_filters", emptySet())
|
||||
|
||||
val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!))
|
||||
|
||||
|
||||
val preferredAudioFormats = getPreferredAudioFormats(resultItem.formats)
|
||||
|
||||
val videoPreferences = VideoPreferences(
|
||||
embedSubs,
|
||||
addChapters, false,
|
||||
ArrayList(sponsorblock),
|
||||
saveSubs,
|
||||
saveAutoSubs,
|
||||
audioFormatIDs = preferredAudioFormats
|
||||
)
|
||||
|
||||
val extraCommands = when(type){
|
||||
Type.audio -> extraCommandsForAudio
|
||||
Type.video -> extraCommandsForVideo
|
||||
else -> ""
|
||||
}
|
||||
|
||||
return DownloadItem(0,
|
||||
resultItem.url,
|
||||
resultItem.title,
|
||||
resultItem.author,
|
||||
resultItem.thumb,
|
||||
resultItem.duration,
|
||||
type,
|
||||
getFormat(resultItem.formats, type),
|
||||
container!!,
|
||||
"",
|
||||
resultItem.formats,
|
||||
downloadPath!!, resultItem.website,
|
||||
"",
|
||||
resultItem.playlistTitle,
|
||||
audioPreferences,
|
||||
videoPreferences,
|
||||
extraCommands,
|
||||
customFileNameTemplate!!,
|
||||
saveThumb,
|
||||
DownloadRepository.Status.Queued.toString(), 0, null, playlistURL = resultItem.playlistURL, playlistIndex = resultItem.playlistIndex
|
||||
)
|
||||
|
||||
return sharedDownloadViewModel.createDownloadItemFromResult(result, url, givenType)
|
||||
}
|
||||
|
||||
fun createResultItemFromDownload(downloadItem: DownloadItem) : ResultItem {
|
||||
return ResultItem(
|
||||
0,
|
||||
downloadItem.url,
|
||||
downloadItem.title,
|
||||
downloadItem.author,
|
||||
downloadItem.duration,
|
||||
downloadItem.thumb,
|
||||
downloadItem.website,
|
||||
downloadItem.playlistTitle,
|
||||
downloadItem.allFormats,
|
||||
"",
|
||||
arrayListOf(),
|
||||
downloadItem.playlistURL,
|
||||
downloadItem.playlistIndex,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
return sharedDownloadViewModel.createResultItemFromDownload(downloadItem)
|
||||
}
|
||||
|
||||
fun createResultItemFromHistory(downloadItem: HistoryItem) : ResultItem {
|
||||
|
|
@ -353,23 +247,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
}
|
||||
|
||||
fun createEmptyResultItem(url: String) : ResultItem {
|
||||
return ResultItem(
|
||||
0,
|
||||
url,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
arrayListOf(),
|
||||
"",
|
||||
arrayListOf(),
|
||||
"",
|
||||
null,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
return sharedDownloadViewModel.createEmptyResultItem(url)
|
||||
}
|
||||
|
||||
fun switchDownloadType(list: List<DownloadItem>, type: Type) : List<DownloadItem>{
|
||||
|
|
@ -471,166 +349,140 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
|
||||
|
||||
fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
|
||||
val requirements: MutableList<(Format) -> Int> = mutableListOf()
|
||||
requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) 10 else 0}
|
||||
|
||||
sharedPreferences.getString("audio_language", "")?.apply {
|
||||
if (this.isNotBlank()){
|
||||
requirements.add { it: Format -> if (it.lang?.contains(this) == true) 3 else 0 }
|
||||
}
|
||||
}
|
||||
|
||||
requirements.add {it: Format -> if ("^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)) 2 else 0}
|
||||
requirements.add {it: Format -> if (it.container == audioContainer) 1 else 0 }
|
||||
return requirements
|
||||
return sharedDownloadViewModel.getPreferredVideoRequirements()
|
||||
}
|
||||
|
||||
//requirement and importance
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
|
||||
val requirements: MutableList<(Format) -> Int> = mutableListOf()
|
||||
//format id
|
||||
requirements.add { it: Format -> if (formatIDPreference.contains(it.format_id)) 20 else 0 }
|
||||
//resolutions
|
||||
application.getStringArray(R.array.video_formats_values)
|
||||
.filter { it.contains("_") }
|
||||
.map{ it.split("_")[0].dropLast(1)
|
||||
}.toMutableList().apply {
|
||||
when(videoQualityPreference) {
|
||||
"worst" -> {
|
||||
requirements.add { it: Format -> if (it.format_note.contains("worst", ignoreCase = true)) (15) else 0 }
|
||||
}
|
||||
"best" -> {
|
||||
requirements.add { it: Format -> if (it.format_note.contains("best", ignoreCase = true)) (15) else 0 }
|
||||
}
|
||||
else -> {
|
||||
val preferenceIndex = this.indexOfFirst { videoQualityPreference.contains(it) }
|
||||
val preference = this[preferenceIndex]
|
||||
for(i in 0..preferenceIndex){
|
||||
removeAt(0)
|
||||
}
|
||||
add(0, preference)
|
||||
forEachIndexed { index, res ->
|
||||
requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (15 - index - 1) else 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
requirements.add { it: Format -> if ("^(${videoCodec})(.+)?$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)) 5 else 0 }
|
||||
requirements.add { it: Format -> if (it.acodec == "none" || it.acodec == "") 1 else 0 }
|
||||
requirements.add { it: Format ->
|
||||
if (videoContainer == "mp4")
|
||||
if (it.container.equals("mpeg_4", true)) 1 else 0
|
||||
else
|
||||
if (it.container.equals(videoContainer, true)) 1 else 0
|
||||
}
|
||||
return requirements
|
||||
return sharedDownloadViewModel.getPreferredVideoRequirements()
|
||||
}
|
||||
|
||||
fun getFormat(formats: List<Format>, type: Type) : Format {
|
||||
when(type) {
|
||||
Type.audio -> {
|
||||
return cloneFormat (
|
||||
try {
|
||||
val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
|
||||
val requirements = getPreferredAudioRequirements()
|
||||
theFormats.maxByOrNull { f -> requirements.sumOf{ req -> req(f)} } ?: throw Exception()
|
||||
}catch (e: Exception){
|
||||
bestAudioFormat
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
Type.video -> {
|
||||
return cloneFormat(
|
||||
try {
|
||||
val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty {
|
||||
defaultVideoFormats.sortedByDescending { it.filesize }
|
||||
}
|
||||
when (videoQualityPreference) {
|
||||
"worst" -> {
|
||||
theFormats.last()
|
||||
}
|
||||
else /*best*/ -> {
|
||||
val requirements = getPreferredVideoRequirements()
|
||||
theFormats.run {
|
||||
if (sharedPreferences.getBoolean("prefer_smaller_formats", false)){
|
||||
sortedBy { it.filesize }.maxByOrNull { f -> requirements.sumOf { req -> req(f) } } ?: throw Exception()
|
||||
}else{
|
||||
sortedByDescending { it.filesize }.maxByOrNull { f ->
|
||||
val summ = requirements.sumOf { req -> req(f) }
|
||||
summ
|
||||
} ?: throw Exception()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch (e: Exception){
|
||||
bestVideoFormat
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val lastUsedCommandTemplate = sharedPreferences.getString("lastCommandTemplateUsed", "")!!
|
||||
val c = if (lastUsedCommandTemplate.isBlank()){
|
||||
commandTemplateDao.getFirst() ?: CommandTemplate(0,"","", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
|
||||
}else{
|
||||
commandTemplateDao.getTemplateByContent(lastUsedCommandTemplate) ?: CommandTemplate(0, "", lastUsedCommandTemplate, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
|
||||
}
|
||||
return generateCommandFormat(c)
|
||||
}
|
||||
}
|
||||
return sharedDownloadViewModel.getFormat(formats, type)
|
||||
}
|
||||
|
||||
fun getPreferredAudioFormats(formats: List<Format>) : ArrayList<String>{
|
||||
val preferredAudioFormats = arrayListOf<String>()
|
||||
for (f in formats.sortedBy { it.format_id }){
|
||||
val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
|
||||
if (fId != null) {
|
||||
if (fId.split("+").all { formats.map { f-> f.format_id }.contains(it) }){
|
||||
preferredAudioFormats.addAll(fId.split("+"))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (preferredAudioFormats.isEmpty()){
|
||||
val audioF = getFormat(formats, Type.audio)
|
||||
if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){
|
||||
preferredAudioFormats.add(audioF.format_id)
|
||||
}
|
||||
}
|
||||
return preferredAudioFormats
|
||||
return sharedDownloadViewModel.getPreferredAudioFormats(formats)
|
||||
}
|
||||
|
||||
fun generateCommandFormat(c: CommandTemplate) : Format {
|
||||
return Format(
|
||||
c.title,
|
||||
c.id.toString(),
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
c.content.replace("\n", " ")
|
||||
)
|
||||
return sharedDownloadViewModel.generateCommandFormat(c)
|
||||
}
|
||||
|
||||
data class ProcessingItemsJob(
|
||||
var jobID: Long = 0,
|
||||
var job: Job? = null,
|
||||
var itemType: String,
|
||||
var itemIDs: List<Long>
|
||||
)
|
||||
|
||||
|
||||
fun getLatestCommandTemplateAsFormat() : Format {
|
||||
val t = commandTemplateDao.getFirst()!!
|
||||
return Format(t.title, "", "", "", "", 0, t.content)
|
||||
}
|
||||
|
||||
fun turnResultItemsToDownloadItems(items: List<ResultItem?>) : List<DownloadItem> {
|
||||
val list : MutableList<DownloadItem> = mutableListOf()
|
||||
items.forEach {
|
||||
val preferredType = getDownloadType(url = it!!.url).toString()
|
||||
list.add(createDownloadItemFromResult(result = it, givenType = Type.valueOf(
|
||||
preferredType
|
||||
)))
|
||||
val loadingProcessingItems: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
var loadingProcessingDownloadsJobs: MutableStateFlow<List<ProcessingItemsJob>> = MutableStateFlow(listOf())
|
||||
private fun cancelAllLoadingProcessingDownloads() {
|
||||
loadingProcessingDownloadsJobs.value.onEach {
|
||||
it.job?.cancel(CancellationException())
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun getLoadingProcessingDownloadJob(jobID: Long) : ProcessingItemsJob? {
|
||||
return loadingProcessingDownloadsJobs.value.firstOrNull { it.jobID == jobID }
|
||||
}
|
||||
|
||||
fun cancelLoadingProcessingDownloads(jobID: Long) {
|
||||
loadingProcessingDownloadsJobs.value.firstOrNull {it.jobID == jobID }?.apply {
|
||||
this.job?.cancel(CancellationException())
|
||||
}
|
||||
}
|
||||
|
||||
fun turnDownloadItemsToProcessingDownloads(itemIDs: List<Long>) : Long {
|
||||
val jobID = System.currentTimeMillis()
|
||||
val job = viewModelScope.launch(Dispatchers.IO) {
|
||||
val insertedIDs = mutableListOf<Long>()
|
||||
try {
|
||||
loadingProcessingItems.emit(true)
|
||||
|
||||
itemIDs.chunked(100).forEach { ids ->
|
||||
val items = repository.getAllItemsByIDs(ids)
|
||||
items.apply {
|
||||
if (!isActive) {
|
||||
throw CancellationException()
|
||||
}
|
||||
this.forEach {
|
||||
it.id = 0
|
||||
it.status = DownloadRepository.Status.Processing.toString()
|
||||
insertedIDs.add(repository.insert(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadingProcessingItems.emit(false)
|
||||
} catch (e: Exception) {
|
||||
repository.deleteAllWithIDs(insertedIDs)
|
||||
loadingProcessingItems.emit(false)
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val currentJobs = loadingProcessingDownloadsJobs.value.toMutableList()
|
||||
currentJobs.add(
|
||||
ProcessingItemsJob(jobID, job, DownloadItem::class.java.toString(), itemIDs)
|
||||
)
|
||||
loadingProcessingDownloadsJobs.emit(currentJobs)
|
||||
}
|
||||
|
||||
return jobID
|
||||
}
|
||||
|
||||
|
||||
fun turnResultItemsToProcessingDownloads(itemIds: List<Long>, downloadNow: Boolean = false) : Long {
|
||||
val jobID = System.currentTimeMillis()
|
||||
val job = viewModelScope.launch(Dispatchers.IO) {
|
||||
val insertedIds = mutableListOf<Long>()
|
||||
try {
|
||||
loadingProcessingItems.emit(true)
|
||||
|
||||
itemIds.chunked(100).forEach { ids ->
|
||||
val items = resultRepository.getAllByIDs(ids)
|
||||
val downloadItems = items.map {
|
||||
val preferredType = getDownloadType(url = it.url).toString()
|
||||
val tmp = createDownloadItemFromResult(result = it, givenType = Type.valueOf(
|
||||
preferredType
|
||||
))
|
||||
tmp.status = DownloadRepository.Status.Processing.toString()
|
||||
tmp
|
||||
}
|
||||
if (!isActive) {
|
||||
throw CancellationException()
|
||||
}
|
||||
|
||||
if (downloadNow) {
|
||||
downloadItems.forEach {
|
||||
it.status = DownloadRepository.Status.Queued.toString()
|
||||
}
|
||||
queueDownloads(downloadItems)
|
||||
}else{
|
||||
downloadItems.forEach {
|
||||
insertedIds.add(repository.insert(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
loadingProcessingItems.emit(false)
|
||||
}catch (e: Exception) {
|
||||
repository.deleteAllWithIDs(insertedIds)
|
||||
loadingProcessingItems.emit(false)
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val currentJobs = loadingProcessingDownloadsJobs.value.toMutableList()
|
||||
currentJobs.add(
|
||||
ProcessingItemsJob(jobID, job, ResultItem::class.java.toString(), itemIds)
|
||||
)
|
||||
loadingProcessingDownloadsJobs.emit(currentJobs)
|
||||
}
|
||||
|
||||
return jobID
|
||||
}
|
||||
|
||||
fun insert(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){
|
||||
|
|
@ -688,6 +540,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
}
|
||||
|
||||
fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) {
|
||||
cancelAllLoadingProcessingDownloads()
|
||||
repository.cancelActiveQueued()
|
||||
}
|
||||
|
||||
|
|
@ -706,11 +559,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
return dao.getSavedDownloadsList()
|
||||
}
|
||||
|
||||
private fun cloneFormat(item: Format) : Format {
|
||||
val string = Gson().toJson(item, Format::class.java)
|
||||
return Gson().fromJson(string, Format::class.java)
|
||||
}
|
||||
|
||||
fun getActiveDownloads() : List<DownloadItem>{
|
||||
return repository.getActiveDownloads()
|
||||
}
|
||||
|
|
@ -725,6 +573,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
|
||||
suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
|
||||
dbManager.downloadDao.resetScheduleTimeForItems(items)
|
||||
dbManager.downloadDao.resetPausedItems()
|
||||
repository.startDownloadWorker(emptyList(), application)
|
||||
}
|
||||
|
||||
|
|
@ -789,173 +638,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
dao.updateDownloadID(-current, id)
|
||||
}
|
||||
|
||||
suspend fun reQueueDownloadItems(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
|
||||
fun reQueueDownloadItems(items: List<Long>) = viewModelScope.launch(Dispatchers.IO) {
|
||||
dbManager.downloadDao.reQueueDownloadItems(items)
|
||||
repository.startDownloadWorker(emptyList(), application)
|
||||
}
|
||||
|
||||
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : Pair<List<Long>, List<Long>> {
|
||||
val context = App.instance
|
||||
val alarmScheduler = AlarmScheduler(context)
|
||||
val activeAndQueuedDownloads = withContext(Dispatchers.IO){
|
||||
repository.getActiveAndQueuedDownloads()
|
||||
}
|
||||
val queuedItems = mutableListOf<DownloadItem>()
|
||||
val existingDownloads = mutableListOf<Long>()
|
||||
val existingHistory = mutableListOf<Long>()
|
||||
|
||||
//if scheduler is on
|
||||
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
|
||||
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
|
||||
alarmScheduler.schedule()
|
||||
}
|
||||
|
||||
if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){
|
||||
items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" }
|
||||
}
|
||||
|
||||
val downloadArchive = runCatching { File(FileUtil.getDownloadArchivePath(application)).useLines { it.toList() } }.getOrElse { listOf() }
|
||||
.map { it.split(" ")[1] }
|
||||
items.forEach {
|
||||
if (! listOf(DownloadRepository.Status.ActivePaused, DownloadRepository.Status.Scheduled).toListString().contains(it.status))
|
||||
it.status = DownloadRepository.Status.Queued.toString()
|
||||
var alreadyExists = false
|
||||
|
||||
val checkDuplicate = sharedPreferences.getString("prevent_duplicate_downloads", "")!!
|
||||
if (checkDuplicate.isNotEmpty() && !ign){
|
||||
when(checkDuplicate){
|
||||
"download_archive" -> {
|
||||
if (downloadArchive.any { d -> it.url.contains(d) }){
|
||||
alreadyExists = true
|
||||
if (it.id == 0L) {
|
||||
it.status = DownloadRepository.Status.Processing.toString()
|
||||
val id = runBlocking {
|
||||
repository.insert(it)
|
||||
}
|
||||
it.id = id
|
||||
}
|
||||
existingDownloads.add(it.id)
|
||||
}
|
||||
}
|
||||
"url_type" -> {
|
||||
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
|
||||
d.id = 0
|
||||
d.logID = null
|
||||
d.customFileNameTemplate = it.customFileNameTemplate
|
||||
d.status = DownloadRepository.Status.Queued.toString()
|
||||
d.toString() == it.toString()
|
||||
}
|
||||
|
||||
if (existingDownload != null){
|
||||
it.status = DownloadRepository.Status.Processing.toString()
|
||||
val id = runBlocking {
|
||||
repository.insert(it)
|
||||
}
|
||||
alreadyExists = true
|
||||
existingDownloads.add(id)
|
||||
}else{
|
||||
//check if downloaded and file exists
|
||||
val history = withContext(Dispatchers.IO){
|
||||
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
|
||||
}
|
||||
|
||||
val existingHistoryItem = history.firstOrNull {
|
||||
h -> h.type == it.type
|
||||
}
|
||||
|
||||
if (existingHistoryItem != null){
|
||||
alreadyExists = true
|
||||
existingHistory.add(existingHistoryItem.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
"config" -> {
|
||||
val currentCommand = infoUtil.buildYoutubeDLRequest(it)
|
||||
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
|
||||
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
|
||||
d.id = 0
|
||||
d.logID = null
|
||||
d.customFileNameTemplate = it.customFileNameTemplate
|
||||
d.status = DownloadRepository.Status.Queued.toString()
|
||||
d.toString() == it.toString()
|
||||
}
|
||||
|
||||
if (existingDownload != null){
|
||||
it.status = DownloadRepository.Status.Processing.toString()
|
||||
val id = runBlocking {
|
||||
repository.insert(it)
|
||||
}
|
||||
alreadyExists = true
|
||||
existingDownloads.add(id)
|
||||
}else{
|
||||
//check if downloaded and file exists
|
||||
val history = withContext(Dispatchers.IO){
|
||||
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
|
||||
}
|
||||
|
||||
val existingHistoryItem = history.firstOrNull {
|
||||
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
|
||||
}
|
||||
|
||||
if (existingHistoryItem != null){
|
||||
alreadyExists = true
|
||||
existingHistory.add(existingHistoryItem.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyExists){
|
||||
if (it.id == 0L){
|
||||
val id = runBlocking {
|
||||
repository.insert(it)
|
||||
}
|
||||
it.id = id
|
||||
}else if (listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.Scheduled).toListString().contains(it.status)){
|
||||
withContext(Dispatchers.IO){
|
||||
repository.update(it)
|
||||
}
|
||||
}
|
||||
|
||||
queuedItems.add(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (existingDownloads.isNotEmpty() || existingHistory.isNotEmpty()){
|
||||
alreadyExistsUiState.update { u -> u.copy(existingHistory, existingDownloads) }
|
||||
}
|
||||
|
||||
if (queuedItems.isNotEmpty()){
|
||||
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime()){
|
||||
repository.startDownloadWorker(queuedItems, context)
|
||||
|
||||
if(!useScheduler){
|
||||
queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
runCatching {
|
||||
resultRepository.updateDownloadItem(it)?.apply {
|
||||
repository.updateWithoutUpsert(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
runCatching {
|
||||
resultRepository.updateDownloadItem(it)?.apply {
|
||||
repository.updateWithoutUpsert(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(existingDownloads, existingHistory)
|
||||
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : List<SharedDownloadViewModel.AlreadyExistsIDs> {
|
||||
val ids = sharedDownloadViewModel.queueDownloads(items, ign)
|
||||
return ids
|
||||
}
|
||||
|
||||
fun getQueuedCollectedFileSize() : Long {
|
||||
|
|
@ -978,19 +668,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
dao.updateProcessingtoSavedStatus()
|
||||
}
|
||||
|
||||
suspend fun downloadProcessingDownloads(timeInMillis: Long = 0){
|
||||
repository.getProcessingDownloads().apply {
|
||||
if (timeInMillis > 0){
|
||||
this.forEach {
|
||||
it.status = DownloadRepository.Status.Scheduled.toString()
|
||||
it.downloadStartTime = timeInMillis
|
||||
}
|
||||
}
|
||||
|
||||
queueDownloads(this)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateProcessingFormat(selectedFormats: List<FormatTuple>): List<Long> {
|
||||
val items = repository.getProcessingDownloads()
|
||||
items.forEachIndexed { index, i ->
|
||||
|
|
@ -1098,16 +775,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
val first = dao.getFirstProcessingDownload()
|
||||
}
|
||||
|
||||
suspend fun addDownloadsToProcessing(ids: List<Long>) {
|
||||
repository.deleteProcessing()
|
||||
val items = repository.getAllItemsByIDs(ids)
|
||||
items.forEach {
|
||||
it.id = 0
|
||||
it.status = DownloadRepository.Status.Processing.toString()
|
||||
insert(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun getURLsByStatus(list: List<DownloadRepository.Status>) : List<String> {
|
||||
return dao.getURLsByStatus(list.map { it.toString() })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.preference.PreferenceManager
|
|||
import androidx.work.Constraints
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
|
|
@ -124,7 +125,15 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
|
|||
}
|
||||
|
||||
//schedule for next time
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
|
||||
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
|
||||
|
||||
val workConstraints = Constraints.Builder()
|
||||
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
else {
|
||||
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
}
|
||||
|
||||
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
|
||||
.addTag("observeSources")
|
||||
.addTag(it.id.toString())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
|
|
@ -34,9 +35,11 @@ import com.deniscerri.ytdl.MainActivity
|
|||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.database.viewmodel.CookieViewModel
|
||||
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
|
||||
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
|
||||
import com.deniscerri.ytdl.ui.BaseActivity
|
||||
import com.deniscerri.ytdl.util.ThemeUtil
|
||||
import com.deniscerri.ytdl.util.UiUtil
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -52,6 +55,7 @@ class ShareActivity : BaseActivity() {
|
|||
|
||||
lateinit var context: Context
|
||||
private lateinit var resultViewModel: ResultViewModel
|
||||
private lateinit var historyViewModel: HistoryViewModel
|
||||
private lateinit var downloadViewModel: DownloadViewModel
|
||||
private lateinit var cookieViewModel: CookieViewModel
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
|
|
@ -115,6 +119,7 @@ class ShareActivity : BaseActivity() {
|
|||
|
||||
context = baseContext
|
||||
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
|
||||
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
|
||||
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
|
@ -208,6 +213,18 @@ class ShareActivity : BaseActivity() {
|
|||
}
|
||||
this@ShareActivity.finish()
|
||||
}
|
||||
|
||||
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
|
||||
if (res.isNotEmpty()){
|
||||
withContext(Dispatchers.Main){
|
||||
val bundle = bundleOf(
|
||||
Pair("duplicates", res)
|
||||
)
|
||||
navController.navigate(R.id.downloadsAlreadyExistDialog2, bundle)
|
||||
}
|
||||
downloadViewModel.alreadyExistsUiState.value = mutableListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.isVisible
|
||||
|
|
@ -59,12 +60,14 @@ import com.google.android.material.search.SearchBar
|
|||
import com.google.android.material.search.SearchView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggestionsAdapter.OnItemClickListener, OnClickListener {
|
||||
|
|
@ -76,7 +79,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
private var downloadSelectedFab: ExtendedFloatingActionButton? = null
|
||||
private var downloadAllFab: ExtendedFloatingActionButton? = null
|
||||
private var clipboardFab: ExtendedFloatingActionButton? = null
|
||||
private var homeFabs: CoordinatorLayout? = null
|
||||
private var homeFabs: LinearLayout? = null
|
||||
private var infoUtil: InfoUtil? = null
|
||||
private var downloadQueue: ArrayList<ResultItem>? = null
|
||||
|
||||
|
|
@ -216,12 +219,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
inputQueries!!.addAll(argList)
|
||||
}
|
||||
|
||||
if(arguments?.getBoolean("search") == true){
|
||||
requireView().post {
|
||||
searchBar?.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
if (inputQueries != null) {
|
||||
lifecycleScope.launch(Dispatchers.IO){
|
||||
resultViewModel.deleteAll()
|
||||
|
|
@ -276,22 +273,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
lifecycleScope.launch {
|
||||
launch{
|
||||
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
|
||||
if (res.downloadItems.isNotEmpty() || res.historyItems.isNotEmpty()) {
|
||||
if (res.isNotEmpty()){
|
||||
withContext(Dispatchers.Main){
|
||||
kotlin.runCatching {
|
||||
UiUtil.handleExistingDownloadsResponse(
|
||||
requireActivity(),
|
||||
requireActivity().lifecycleScope,
|
||||
requireActivity().supportFragmentManager,
|
||||
res,
|
||||
downloadViewModel,
|
||||
historyViewModel)
|
||||
}
|
||||
val bundle = bundleOf(
|
||||
Pair("duplicates", ArrayList(res))
|
||||
)
|
||||
delay(500)
|
||||
findNavController().navigate(R.id.downloadsAlreadyExistDialog, bundle)
|
||||
}
|
||||
downloadViewModel.alreadyExistsUiState.value = DownloadViewModel.AlreadyExistsUIState(
|
||||
mutableListOf(),
|
||||
mutableListOf()
|
||||
)
|
||||
downloadViewModel.alreadyExistsUiState.value = mutableListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -313,15 +303,24 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
arguments?.remove("showDownloadsWithUpdatedFormats")
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val ids = arguments?.getLongArray("downloadIds") ?: return@launch
|
||||
downloadViewModel.updateItemsWithIdsToProcessingStatus(ids.toList())
|
||||
val jobID = downloadViewModel.turnDownloadItemsToProcessingDownloads(ids.toList())
|
||||
withContext(Dispatchers.Main){
|
||||
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
|
||||
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
|
||||
Pair("processingDownloadsJobID", jobID)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(arguments?.getBoolean("search") == true){
|
||||
arguments?.remove("search")
|
||||
requireView().post {
|
||||
searchBar?.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){
|
||||
updateSearchViewItems(searchView?.editText?.text)
|
||||
updateSearchViewItems(searchView?.editText?.text.toString())
|
||||
}
|
||||
|
||||
requireView().post {
|
||||
|
|
@ -405,13 +404,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
chipGroupDivider?.visibility = VISIBLE
|
||||
}
|
||||
|
||||
updateSearchViewItems(searchView!!.editText.text)
|
||||
updateSearchViewItems(searchView!!.editText.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
searchView!!.editText.doAfterTextChanged {
|
||||
if (searchView!!.currentTransitionState != SearchView.TransitionState.SHOWN) return@doAfterTextChanged
|
||||
updateSearchViewItems(it)
|
||||
updateSearchViewItems(it.toString())
|
||||
}
|
||||
|
||||
searchView!!.editText.setOnTouchListener(OnTouchListener { _, event ->
|
||||
|
|
@ -454,7 +453,11 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
searchBar!!.setOnMenuItemClickListener { m: MenuItem ->
|
||||
when (m.itemId) {
|
||||
R.id.delete_results -> {
|
||||
resultViewModel.cancelParsingQueries()
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
resultViewModel.cancelParsingQueries()
|
||||
}
|
||||
}
|
||||
resultViewModel.getTrending()
|
||||
selectedObjects = ArrayList()
|
||||
searchBar!!.setText("")
|
||||
|
|
@ -479,7 +482,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun updateSearchViewItems(searchQuery: Editable?) = lifecycleScope.launch(Dispatchers.Main) {
|
||||
private fun updateSearchViewItems(searchQuery: String) = lifecycleScope.launch(Dispatchers.Main) {
|
||||
lifecycleScope.launch {
|
||||
if (searchView!!.editText.text.isEmpty()){
|
||||
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
|
|
@ -490,13 +493,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
val combinedList = mutableListOf<SearchSuggestionItem>()
|
||||
|
||||
val history = withContext(Dispatchers.IO){
|
||||
resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery!!) }
|
||||
resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery) }
|
||||
}.map {
|
||||
SearchSuggestionItem(it, SearchSuggestionType.HISTORY)
|
||||
}
|
||||
val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){
|
||||
withContext(Dispatchers.IO){
|
||||
infoUtil!!.getSearchSuggestions(searchQuery.toString())
|
||||
infoUtil!!.getSearchSuggestions(searchQuery)
|
||||
}
|
||||
}else{
|
||||
emptyList()
|
||||
|
|
@ -509,12 +512,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
|
||||
val url = checkClipboard()
|
||||
url?.apply {
|
||||
if (this.isNotEmpty()){
|
||||
var alreadyHasThem = this.all { queriesChipGroup?.children?.any { c -> (c as Chip).text.contains(it) } == true }
|
||||
if (this.isNotEmpty() && !alreadyHasThem){
|
||||
combinedList.add(0, SearchSuggestionItem(this.joinToString("\n"), SearchSuggestionType.CLIPBOARD))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
searchSuggestionsAdapter?.submitList(combinedList)
|
||||
|
||||
// history.forEach { s ->
|
||||
|
|
@ -729,21 +732,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
} catch (e: Exception) {""}
|
||||
if (viewIdName.isNotEmpty()) {
|
||||
if (viewIdName == "downloadAll") {
|
||||
lifecycleScope.launch {
|
||||
val downloadList = withContext(Dispatchers.IO){
|
||||
downloadViewModel.turnResultItemsToDownloadItems(resultsList!!)
|
||||
}
|
||||
if (sharedPreferences!!.getBoolean("download_card", true)) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
downloadViewModel.insertToProcessing(downloadList)
|
||||
}
|
||||
|
||||
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
|
||||
} else {
|
||||
downloadList.chunked(100).forEach {
|
||||
downloadViewModel.queueDownloads(it)
|
||||
}
|
||||
}
|
||||
val showDownloadCard = sharedPreferences!!.getBoolean("download_card", true)
|
||||
val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(resultsList!!.map { it!!.id }, downloadNow = !showDownloadCard)
|
||||
if (showDownloadCard){
|
||||
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
|
||||
Pair("processingDownloadsJobID", jobID)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -806,24 +800,18 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
}
|
||||
R.id.download -> {
|
||||
lifecycleScope.launch {
|
||||
if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects.size == 1) {
|
||||
val showDownloadCard = sharedPreferences!!.getBoolean("download_card", true)
|
||||
if (showDownloadCard && selectedObjects.size == 1) {
|
||||
showSingleDownloadSheet(
|
||||
selectedObjects[0],
|
||||
downloadViewModel.getDownloadType(url = selectedObjects[0].url)
|
||||
)
|
||||
}else{
|
||||
val downloadList = withContext(Dispatchers.IO){
|
||||
downloadViewModel.turnResultItemsToDownloadItems(selectedObjects)
|
||||
}
|
||||
|
||||
if (sharedPreferences!!.getBoolean("download_card", true)) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
downloadViewModel.insertToProcessing(downloadList)
|
||||
}
|
||||
|
||||
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
|
||||
} else {
|
||||
downloadViewModel.queueDownloads(downloadList)
|
||||
val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(selectedObjects.map { it.id }, downloadNow = !showDownloadCard)
|
||||
if (showDownloadCard){
|
||||
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
|
||||
Pair("processingDownloadsJobID", jobID)
|
||||
))
|
||||
}
|
||||
}
|
||||
clearCheckedItems()
|
||||
|
|
@ -835,7 +823,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
homeAdapter?.checkAll(resultsList)
|
||||
selectedObjects.clear()
|
||||
resultsList?.forEach { selectedObjects.add(it!!) }
|
||||
mode?.title = getString(R.string.all_items_selected)
|
||||
mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}"
|
||||
true
|
||||
}
|
||||
R.id.invert_selected -> {
|
||||
|
|
@ -905,14 +893,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
|
||||
}
|
||||
|
||||
override fun onSearchSuggestionAdd(t: String) {
|
||||
val items = t.split("\n")
|
||||
override fun onSearchSuggestionAdd(text: String) {
|
||||
val items = text.split("\n")
|
||||
|
||||
items.forEach {text ->
|
||||
val present = queriesChipGroup!!.children.firstOrNull { (it as Chip).text.toString() == text }
|
||||
items.forEach {t ->
|
||||
val present = queriesChipGroup!!.children.firstOrNull { (it as Chip).text.toString() == t }
|
||||
if (present == null) {
|
||||
val chip = layoutinflater!!.inflate(R.layout.input_chip, queriesChipGroup, false) as Chip
|
||||
chip.text = text
|
||||
chip.text = t
|
||||
chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK))
|
||||
chip.setOnClickListener {
|
||||
if (queriesChipGroup!!.childCount == 1) queriesConstraint!!.visibility = View.GONE
|
||||
|
|
@ -924,17 +912,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
}
|
||||
|
||||
searchView!!.editText.setText("")
|
||||
if (queriesChipGroup!!.childCount == 0) queriesConstraint!!.visibility = View.GONE
|
||||
else queriesConstraint!!.visibility = View.VISIBLE
|
||||
queriesConstraint?.isVisible = queriesChipGroup?.childCount!! > 0
|
||||
|
||||
val clipBoardItem = searchSuggestionsRecyclerView?.layoutManager?.findViewByPosition(0)
|
||||
clipBoardItem?.apply {
|
||||
if ((this as ConstraintLayout).findViewById<TextView>(R.id.suggestion_text).text == getString(R.string.link_you_copied)){
|
||||
searchSuggestionsAdapter?.notifyItemRemoved(0)
|
||||
searchSuggestionsAdapter?.getList()?.apply {
|
||||
if (this.first().type == SearchSuggestionType.CLIPBOARD){
|
||||
val newList = this.toMutableList().drop(1)
|
||||
searchSuggestionsAdapter?.submitList(newList)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun onSearchSuggestionLongClick(text: String, position: Int) {
|
||||
|
|
@ -943,7 +928,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
|||
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
resultViewModel.removeSearchQueryFromHistory(text)
|
||||
updateSearchViewItems(searchView!!.editText.text)
|
||||
updateSearchViewItems(searchView!!.editText.text.toString())
|
||||
}
|
||||
deleteDialog.show()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,27 +2,34 @@ package com.deniscerri.ytdl.ui.adapter
|
|||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.database.models.AlreadyExistsItem
|
||||
import com.deniscerri.ytdl.database.models.DownloadItem
|
||||
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdl.util.Extensions.loadThumbnail
|
||||
import com.deniscerri.ytdl.util.Extensions.popup
|
||||
import com.deniscerri.ytdl.util.FileUtil
|
||||
import com.deniscerri.ytdl.util.UiUtil
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
|
||||
class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<Pair<DownloadItem, Long?>, AlreadyExistsAdapter.ViewHolder>(AsyncDifferConfig.Builder(
|
||||
class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<AlreadyExistsItem, AlreadyExistsAdapter.ViewHolder>(AsyncDifferConfig.Builder(
|
||||
DIFF_CALLBACK
|
||||
).build()) {
|
||||
private val onItemClickListener: OnItemClickListener
|
||||
|
|
@ -39,7 +46,7 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
|
|||
val cardView: MaterialCardView
|
||||
|
||||
init {
|
||||
cardView = itemView as MaterialCardView
|
||||
cardView = itemView.findViewById(R.id.download_card_view)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,12 +58,9 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
|
|||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val alreadyExistsItem = getItem(position) ?: return
|
||||
val item = alreadyExistsItem.first
|
||||
val historyID = alreadyExistsItem.second
|
||||
|
||||
val card = holder.cardView
|
||||
card.tag = item.id.toString()
|
||||
card.popup()
|
||||
card.tag = alreadyExistsItem.downloadItem.id.toString()
|
||||
val item = alreadyExistsItem.downloadItem
|
||||
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
|
||||
|
|
@ -76,6 +80,20 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
|
|||
}
|
||||
itemTitle.text = title.ifEmpty { item.url }
|
||||
|
||||
//DOWNLOAD TYPE -----------------------------
|
||||
val type = card.findViewById<TextView>(R.id.download_type)
|
||||
when(item.type){
|
||||
DownloadViewModel.Type.audio -> type.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
R.drawable.ic_music_formatcard, 0,0,0
|
||||
)
|
||||
DownloadViewModel.Type.video -> type.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
R.drawable.ic_video_formatcard, 0,0,0
|
||||
)
|
||||
else -> type.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
R.drawable.ic_terminal_formatcard, 0,0,0
|
||||
)
|
||||
}
|
||||
|
||||
val formatNote = card.findViewById<TextView>(R.id.format_note)
|
||||
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
|
||||
View.GONE
|
||||
|
|
@ -102,45 +120,64 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
|
|||
if (fileSizeReadable == "?") fileSize.visibility = View.GONE
|
||||
else fileSize.text = fileSizeReadable
|
||||
|
||||
val editBtn = card.findViewById<Button>(R.id.already_exists_edit)
|
||||
val deleteBtn = card.findViewById<Button>(R.id.already_exists_delete)
|
||||
val menu = card.findViewById<View>(R.id.options)
|
||||
menu.isVisible = true
|
||||
menu.setOnClickListener {
|
||||
val popup = PopupMenu(activity, it)
|
||||
popup.menuInflater.inflate(R.menu.already_exists_menu, popup.menu)
|
||||
if (Build.VERSION.SDK_INT > 27) popup.menu.setGroupDividerEnabled(true)
|
||||
|
||||
popup.setOnMenuItemClickListener { m ->
|
||||
when(m.itemId){
|
||||
R.id.edit -> {
|
||||
onItemClickListener.onEditItem(alreadyExistsItem, position)
|
||||
popup.dismiss()
|
||||
}
|
||||
R.id.delete -> {
|
||||
onItemClickListener.onDeleteItem(alreadyExistsItem, position)
|
||||
popup.dismiss()
|
||||
}
|
||||
R.id.copy_url -> {
|
||||
UiUtil.copyLinkToClipBoard(activity, item.url)
|
||||
popup.dismiss()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
popup.show()
|
||||
|
||||
}
|
||||
|
||||
card.setOnLongClickListener {
|
||||
onItemClickListener.onDeleteItem(item, position, historyID)
|
||||
onItemClickListener.onDeleteItem(alreadyExistsItem, position)
|
||||
true
|
||||
}
|
||||
|
||||
editBtn.setOnClickListener {
|
||||
onItemClickListener.onEditItem(item, position)
|
||||
}
|
||||
|
||||
deleteBtn.setOnClickListener {
|
||||
onItemClickListener.onDeleteItem(item, position, historyID)
|
||||
}
|
||||
|
||||
card.setOnClickListener(null)
|
||||
if (historyID != null){
|
||||
if (alreadyExistsItem.historyID != null){
|
||||
card.setOnClickListener {
|
||||
onItemClickListener.onShowHistoryItem(historyID)
|
||||
onItemClickListener.onShowHistoryItem(alreadyExistsItem.historyID!!)
|
||||
}
|
||||
}else{
|
||||
card.setOnClickListener(null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onEditItem(downloadItem: DownloadItem, position: Int)
|
||||
fun onDeleteItem(downloadItem: DownloadItem, position: Int, historyID: Long?)
|
||||
fun onShowHistoryItem(id: Long)
|
||||
fun onEditItem(alreadyExistsItem: AlreadyExistsItem, position: Int)
|
||||
fun onDeleteItem(alreadyExistsItem: AlreadyExistsItem, position: Int)
|
||||
fun onShowHistoryItem(historyItemID: Long)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DIFF_CALLBACK: DiffUtil.ItemCallback<Pair<DownloadItem, Long?>> = object : DiffUtil.ItemCallback<Pair<DownloadItem, Long?>>() {
|
||||
override fun areItemsTheSame(oldItem: Pair<DownloadItem, Long?>, newItem: Pair<DownloadItem, Long?>): Boolean {
|
||||
return oldItem.first.id == newItem.first.id
|
||||
private val DIFF_CALLBACK: DiffUtil.ItemCallback<AlreadyExistsItem> = object : DiffUtil.ItemCallback<AlreadyExistsItem>() {
|
||||
override fun areItemsTheSame(oldItem: AlreadyExistsItem, newItem: AlreadyExistsItem): Boolean {
|
||||
return oldItem.downloadItem.id == newItem.downloadItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Pair<DownloadItem, Long?>, newItem: Pair<DownloadItem, Long?>): Boolean {
|
||||
return oldItem.first.id == newItem.first.id && oldItem.first.title == newItem.first.title && oldItem.first.author == newItem.first.author && oldItem.first.thumb == newItem.first.thumb
|
||||
override fun areContentsTheSame(oldItem: AlreadyExistsItem, newItem: AlreadyExistsItem): Boolean {
|
||||
return oldItem.downloadItem.id == newItem.downloadItem.id && oldItem.downloadItem.title == newItem.downloadItem.title && oldItem.downloadItem.author == newItem.downloadItem.author && oldItem.downloadItem.thumb == newItem.downloadItem.thumb
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ class ScheduledDownloadAdapter(onItemClickListener: OnItemClickListener, activit
|
|||
|
||||
if (item == null) return
|
||||
card.tag = item.id.toString()
|
||||
holder.itemView.tag = item.id.toString()
|
||||
|
||||
//Scheduled Time
|
||||
val time = mainView.findViewById<TextView>(R.id.scheduled_time)
|
||||
|
|
@ -152,7 +153,7 @@ class ScheduledDownloadAdapter(onItemClickListener: OnItemClickListener, activit
|
|||
if (checkedItems.size > 0 || inverted) {
|
||||
checkCard(card, item.id, position)
|
||||
} else {
|
||||
onItemClickListener.onCardClick(item.id)
|
||||
onItemClickListener.onCardClick(item.id, position)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -218,7 +219,7 @@ class ScheduledDownloadAdapter(onItemClickListener: OnItemClickListener, activit
|
|||
|
||||
interface OnItemClickListener {
|
||||
fun onActionButtonClick(itemID: Long)
|
||||
fun onCardClick(itemID: Long)
|
||||
fun onCardClick(itemID: Long, position: Int)
|
||||
fun onCardSelect(isChecked: Boolean, position: Int)
|
||||
}
|
||||
|
||||
|
|
@ -230,7 +231,11 @@ class ScheduledDownloadAdapter(onItemClickListener: OnItemClickListener, activit
|
|||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean {
|
||||
return oldItem.id == newItem.id && oldItem.title == newItem.title && oldItem.author == newItem.author && oldItem.thumb == newItem.thumb
|
||||
return oldItem.id == newItem.id &&
|
||||
oldItem.title == newItem.title &&
|
||||
oldItem.author == newItem.author &&
|
||||
oldItem.thumb == newItem.thumb &&
|
||||
oldItem.downloadStartTime == newItem.downloadStartTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activit
|
|||
val linear = holder.linear
|
||||
when (item.type){
|
||||
SearchSuggestionType.SUGGESTION -> {
|
||||
holder.itemView.tag = SearchSuggestionType.SUGGESTION.toString()
|
||||
val textView = linear.findViewById<TextView>(R.id.suggestion_text)
|
||||
textView.text = item.text
|
||||
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_search, 0, 0, 0)
|
||||
|
|
@ -67,6 +68,7 @@ class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activit
|
|||
}
|
||||
}
|
||||
SearchSuggestionType.HISTORY -> {
|
||||
holder.itemView.tag = SearchSuggestionType.HISTORY.toString()
|
||||
val textView = linear.findViewById<TextView>(R.id.suggestion_text)
|
||||
textView.text = item.text
|
||||
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0)
|
||||
|
|
@ -85,6 +87,7 @@ class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activit
|
|||
}
|
||||
}
|
||||
SearchSuggestionType.CLIPBOARD -> {
|
||||
holder.itemView.tag = SearchSuggestionType.CLIPBOARD.toString()
|
||||
val textView = linear.findViewById<TextView>(R.id.suggestion_text)
|
||||
textView.text = activity.getString(R.string.link_you_copied)
|
||||
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_language, 0, 0, 0)
|
||||
|
|
@ -102,6 +105,9 @@ class SearchSuggestionsAdapter(onItemClickListener: OnItemClickListener, activit
|
|||
}
|
||||
}
|
||||
}
|
||||
fun getList() : List<SearchSuggestionItem> {
|
||||
return this.currentList
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onSearchSuggestionClick(text: String)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ import android.view.KeyEvent
|
|||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.widget.*
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.children
|
||||
|
|
@ -28,12 +30,17 @@ import androidx.preference.PreferenceManager
|
|||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.database.models.ChapterItem
|
||||
import com.deniscerri.ytdl.database.models.DownloadItem
|
||||
import com.deniscerri.ytdl.util.Extensions
|
||||
import com.deniscerri.ytdl.util.Extensions.convertToTimestamp
|
||||
import com.deniscerri.ytdl.util.Extensions.setTextAndRecalculateWidth
|
||||
import com.deniscerri.ytdl.util.Extensions.toStringDuration
|
||||
import com.deniscerri.ytdl.util.Extensions.toStringTimeStamp
|
||||
import com.deniscerri.ytdl.util.Extensions.toTimePeriodsArray
|
||||
import com.deniscerri.ytdl.util.InfoUtil
|
||||
import com.deniscerri.ytdl.util.UiUtil
|
||||
import com.deniscerri.ytdl.util.VideoPlayerUtil
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
|
|
@ -55,6 +62,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
||||
|
|
@ -68,10 +76,11 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
private lateinit var progress : ProgressBar
|
||||
private lateinit var pauseBtn : MaterialButton
|
||||
private lateinit var rewindBtn : MaterialButton
|
||||
private lateinit var forwardBtn : MaterialButton
|
||||
private lateinit var muteBtn : MaterialButton
|
||||
private lateinit var rangeSlider : RangeSlider
|
||||
private lateinit var fromTextInput : TextInputLayout
|
||||
private lateinit var toTextInput : TextInputLayout
|
||||
private lateinit var fromTextInput : EditText
|
||||
private lateinit var toTextInput : EditText
|
||||
private lateinit var cancelBtn : Button
|
||||
private lateinit var okBtn : Button
|
||||
private lateinit var forceKeyframes: MaterialSwitch
|
||||
|
|
@ -85,7 +94,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
private lateinit var suggestedLabel : View
|
||||
private lateinit var item: DownloadItem
|
||||
|
||||
private var timeSeconds by Delegates.notNull<Int>()
|
||||
private var itemDurationTimestamp = 0L
|
||||
private lateinit var selectedCuts: MutableList<String>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -121,7 +130,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
val frame = view.findViewById<MaterialCardView>(R.id.frame_layout)
|
||||
val videoView = view.findViewById<PlayerView>(R.id.video_view)
|
||||
videoView.player = player
|
||||
timeSeconds = convertStringToTimestamp(item.duration)
|
||||
itemDurationTimestamp = item.duration.convertToTimestamp()
|
||||
if (chapters == null) chapters = emptyList()
|
||||
|
||||
//cut section
|
||||
|
|
@ -131,10 +140,11 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
progress = view.findViewById(R.id.progress)
|
||||
pauseBtn = view.findViewById(R.id.pause)
|
||||
rewindBtn = view.findViewById(R.id.rewind)
|
||||
forwardBtn = view.findViewById(R.id.forward)
|
||||
muteBtn = view.findViewById(R.id.mute)
|
||||
rangeSlider = view.findViewById(R.id.rangeSlider)
|
||||
fromTextInput = view.findViewById(R.id.from_textinput)
|
||||
toTextInput = view.findViewById(R.id.to_textinput)
|
||||
fromTextInput = view.findViewById(R.id.from_textinput_edittext)
|
||||
toTextInput = view.findViewById(R.id.to_textinput_edittext)
|
||||
cancelBtn = view.findViewById(R.id.cancelButton)
|
||||
okBtn = view.findViewById(R.id.okButton)
|
||||
suggestedChips = view.findViewById(R.id.chapters)
|
||||
|
|
@ -216,12 +226,12 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
videoProgress(player).collect { p ->
|
||||
val currentTime = p.toStringDuration(Locale.US)
|
||||
durationText.text = "$currentTime / ${item.duration}"
|
||||
val startTimestamp = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
|
||||
if (toTextInput.editText!!.text.isNotBlank()){
|
||||
val endTimestamp = convertStringToTimestamp(toTextInput.editText!!.text.toString())
|
||||
if (p >= endTimestamp){
|
||||
val startTimestamp = fromTextInput.text.toString().convertToTimestamp()
|
||||
if (toTextInput.text.isNotBlank()){
|
||||
val endTimestamp = toTextInput.text.toString().convertToTimestamp()
|
||||
if (p >= endTimestamp / 1000 || (!player.isPlaying && p >= endTimestamp / 1000 - 1)){
|
||||
player.prepare()
|
||||
player.seekTo((startTimestamp * 1000).toLong())
|
||||
player.seekTo(startTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,12 +270,22 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
|
||||
rewindBtn.setOnClickListener {
|
||||
try {
|
||||
val seconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
|
||||
player.seekTo((seconds * 1000).toLong())
|
||||
val stmp = fromTextInput.text.toString().convertToTimestamp()
|
||||
player.seekTo(stmp)
|
||||
player.play()
|
||||
}catch (ignored: Exception) {}
|
||||
}
|
||||
|
||||
forwardBtn.setOnClickListener {
|
||||
kotlin.runCatching {
|
||||
val startTimestamp = fromTextInput.text.toString().convertToTimestamp()
|
||||
var endTimestamp = toTextInput.text.toString().convertToTimestamp() - 1500
|
||||
if (endTimestamp < startTimestamp) endTimestamp = startTimestamp
|
||||
player.seekTo(endTimestamp)
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val editor = prefs.edit()
|
||||
|
||||
|
|
@ -279,12 +299,12 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
private fun initCutSection(){
|
||||
fromTextInput.editText!!.setTextAndRecalculateWidth("0:00")
|
||||
toTextInput.editText!!.setTextAndRecalculateWidth(item.duration)
|
||||
fromTextInput.setTextAndRecalculateWidth("0:00")
|
||||
toTextInput.setTextAndRecalculateWidth(item.duration)
|
||||
|
||||
rangeSlider.valueFrom = 0f
|
||||
rangeSlider.valueTo = timeSeconds.toFloat()
|
||||
rangeSlider.setValues(0F, timeSeconds.toFloat())
|
||||
rangeSlider.valueTo = (itemDurationTimestamp / 1000).toFloat()
|
||||
rangeSlider.setValues(0F, (itemDurationTimestamp / 1000).toFloat())
|
||||
rangeSlider.setOnTouchListener { _, event -> // Handle touch events here
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
|
|
@ -298,37 +318,78 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
// Return 'false' to allow the event to continue propagating or 'true' to consume it
|
||||
false
|
||||
}
|
||||
rangeSlider.addOnChangeListener { rangeSlider, fl, b ->
|
||||
|
||||
}
|
||||
// fromTextInput.isFocusable = false
|
||||
// fromTextInput.isClickable = true
|
||||
// fromTextInput.setOnClickListener {
|
||||
// val currentMilliseconds = (it as EditText).text.toString().convertToTimestamp()
|
||||
// showTimestampBottomSheet(true, currentMilliseconds) { new ->
|
||||
// var newTimestamp = new
|
||||
// val endTimestamp = toTextInput.text.toString().convertToTimestamp()
|
||||
// fromTextInput.setTextAndRecalculateWidth(newTimestamp.toStringTimeStamp())
|
||||
//
|
||||
// if (newTimestamp > itemDurationTimestamp) {
|
||||
// newTimestamp = itemDurationTimestamp
|
||||
// }
|
||||
//
|
||||
// rangeSlider.setValues((newTimestamp / 1000).toFloat(), (endTimestamp / 1000).toFloat())
|
||||
// okBtn.isEnabled = newTimestamp != 0L || endTimestamp != itemDurationTimestamp
|
||||
// try {
|
||||
// player.seekTo(newTimestamp)
|
||||
// player.play()
|
||||
// }catch (ignored: Exception) {}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// toTextInput.isFocusable = false
|
||||
// toTextInput.isClickable = true
|
||||
// toTextInput.setOnClickListener {
|
||||
// val currentMilliseconds = (it as EditText).text.toString().convertToTimestamp()
|
||||
// showTimestampBottomSheet(false, currentMilliseconds) { new ->
|
||||
// var newTimestamp = new
|
||||
// val startTimestamp = fromTextInput.text.toString().convertToTimestamp()
|
||||
//
|
||||
// if (newTimestamp > itemDurationTimestamp) {
|
||||
// newTimestamp = itemDurationTimestamp
|
||||
// }
|
||||
//
|
||||
// toTextInput.setTextAndRecalculateWidth(newTimestamp.toStringTimeStamp())
|
||||
// rangeSlider.setValues((startTimestamp / 1000).toFloat(), (newTimestamp / 1000).toFloat())
|
||||
// okBtn.isEnabled = startTimestamp != 0L || newTimestamp != itemDurationTimestamp
|
||||
// try {
|
||||
// player.seekTo(newTimestamp - 1500)
|
||||
// player.play()
|
||||
// }catch (ignored: Exception) {}
|
||||
// }
|
||||
// }
|
||||
|
||||
fromTextInput.editText!!.setOnKeyListener(object : View.OnKeyListener {
|
||||
fromTextInput.setOnKeyListener(object : View.OnKeyListener {
|
||||
|
||||
override fun onKey(p0: View?, keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if ((event!!.action == KeyEvent.ACTION_DOWN) &&
|
||||
(keyCode == KeyEvent.KEYCODE_ENTER)) {
|
||||
|
||||
var startTimestamp = rangeSlider.valueFrom.toInt()
|
||||
val endTimestamp = rangeSlider.valueTo.toInt()
|
||||
|
||||
fromTextInput.editText!!.clearFocus()
|
||||
var seconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
|
||||
val endSeconds = convertStringToTimestamp(toTextInput.editText!!.text.toString())
|
||||
fromTextInput.clearFocus()
|
||||
var timestamp = fromTextInput.text.toString().convertToTimestamp()
|
||||
val endstamp = toTextInput.text.toString().convertToTimestamp()
|
||||
|
||||
if (seconds == 0) {
|
||||
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
|
||||
}else if (seconds > endSeconds){
|
||||
if (timestamp == 0L) {
|
||||
fromTextInput.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
|
||||
}else if (timestamp > endstamp){
|
||||
startTimestamp = 0
|
||||
seconds = 0
|
||||
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
|
||||
timestamp = 0
|
||||
fromTextInput.setTextAndRecalculateWidth(startTimestamp.toStringDuration(Locale.US))
|
||||
}else{
|
||||
fromTextInput.setTextAndRecalculateWidth(fromTextInput.text.toString())
|
||||
}
|
||||
|
||||
fromTextInput.editText!!.setTextAndRecalculateWidth(fromTextInput.editText!!.text.toString())
|
||||
|
||||
rangeSlider.setValues(seconds.toFloat(), endSeconds.toFloat())
|
||||
okBtn.isEnabled = seconds != 0 || endSeconds != timeSeconds
|
||||
rangeSlider.setValues((timestamp / 1000).toFloat(), (endstamp / 1000).toFloat())
|
||||
okBtn.isEnabled = timestamp != 0L || endstamp != itemDurationTimestamp
|
||||
try {
|
||||
player.seekTo(seconds.toLong() * 1000)
|
||||
player.seekTo(timestamp)
|
||||
player.play()
|
||||
}catch (ignored: Exception) {}
|
||||
|
||||
|
|
@ -339,36 +400,32 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
})
|
||||
|
||||
|
||||
toTextInput.editText!!.setOnKeyListener(object : View.OnKeyListener {
|
||||
toTextInput.setOnKeyListener(object : View.OnKeyListener {
|
||||
|
||||
override fun onKey(p0: View?, keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if ((event!!.action == KeyEvent.ACTION_DOWN) &&
|
||||
(keyCode == KeyEvent.KEYCODE_ENTER)) {
|
||||
|
||||
var endTimestamp = (rangeSlider.valueTo.toInt() * timeSeconds) / 100
|
||||
toTextInput.clearFocus()
|
||||
val timestamp = fromTextInput.text.toString().convertToTimestamp()
|
||||
var endstamp = toTextInput.text.toString().convertToTimestamp()
|
||||
|
||||
toTextInput.editText!!.clearFocus()
|
||||
val startSeconds = convertStringToTimestamp(fromTextInput.editText!!.text.toString())
|
||||
var endSeconds = convertStringToTimestamp(toTextInput.editText!!.text.toString())
|
||||
|
||||
if (endSeconds > timeSeconds){
|
||||
endTimestamp = timeSeconds
|
||||
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
|
||||
endSeconds = timeSeconds
|
||||
if (endstamp > itemDurationTimestamp){
|
||||
endstamp = itemDurationTimestamp
|
||||
}
|
||||
if (endstamp == 0L) {
|
||||
endstamp = itemDurationTimestamp
|
||||
}
|
||||
if (endstamp <= timestamp){
|
||||
endstamp = timestamp + 1000
|
||||
}
|
||||
|
||||
if (endSeconds == 0) {
|
||||
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
|
||||
}else if (endSeconds <= rangeSlider.valueFrom.toInt()){
|
||||
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestamp.toStringDuration(Locale.US))
|
||||
}
|
||||
toTextInput.setTextAndRecalculateWidth(endstamp.toStringTimeStamp())
|
||||
|
||||
toTextInput.editText!!.setTextAndRecalculateWidth(toTextInput.editText!!.text.toString())
|
||||
|
||||
rangeSlider.setValues(startSeconds.toFloat(), endSeconds.toFloat())
|
||||
okBtn.isEnabled = startSeconds != 0 || endSeconds != timeSeconds
|
||||
rangeSlider.setValues((timestamp / 1000).toFloat(), (endstamp / 1000).toFloat())
|
||||
okBtn.isEnabled = timestamp != 0L || endstamp != itemDurationTimestamp
|
||||
try {
|
||||
player.seekTo((endSeconds.toLong() - 2L) * 1000)
|
||||
player.seekTo(endstamp - 1500)
|
||||
player.play()
|
||||
}catch (ignored: Exception) {}
|
||||
|
||||
|
|
@ -391,7 +448,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
okBtn.isEnabled = false
|
||||
okBtn.setOnClickListener {
|
||||
forceKeyframes.isVisible = true
|
||||
val chip = createChip("${fromTextInput.editText!!.text}-${toTextInput.editText!!.text}")
|
||||
val chip = createChip("${fromTextInput.text}-${toTextInput.text}")
|
||||
chip.performClick()
|
||||
cutSection.visibility = View.GONE
|
||||
cutListSection.visibility = View.VISIBLE
|
||||
|
|
@ -402,30 +459,30 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
|
||||
private fun updateFromSlider(){
|
||||
val values = rangeSlider.values
|
||||
val startTimestamp = values[0].toInt()
|
||||
val endTimestamp = values[1].toInt()
|
||||
val startTimestamp = values[0].toLong() * 1000
|
||||
val endTimestamp = values[1].toLong() * 1000
|
||||
|
||||
val startTimestampString = startTimestamp.toStringDuration(Locale.US)
|
||||
val endTimestampString = endTimestamp.toStringDuration(Locale.US)
|
||||
val startTimestampString = startTimestamp.toStringTimeStamp()
|
||||
val endTimestampString = endTimestamp.toStringTimeStamp()
|
||||
|
||||
var draggedFromBeginning = true
|
||||
if (toTextInput.editText!!.text.toString() != endTimestampString){
|
||||
if (toTextInput.text.toString() != endTimestampString){
|
||||
draggedFromBeginning = false
|
||||
}
|
||||
|
||||
fromTextInput.editText!!.setTextAndRecalculateWidth(startTimestampString)
|
||||
toTextInput.editText!!.setTextAndRecalculateWidth(endTimestampString)
|
||||
fromTextInput.setTextAndRecalculateWidth(startTimestampString)
|
||||
toTextInput.setTextAndRecalculateWidth(endTimestampString)
|
||||
|
||||
|
||||
okBtn.isEnabled = values[0] != 0F || values[1] != timeSeconds.toFloat()
|
||||
okBtn.isEnabled = values[0] != 0F || values[1] != (itemDurationTimestamp / 1000).toFloat()
|
||||
|
||||
val startpos = (startTimestamp * 1000).toLong()
|
||||
val startpos = startTimestamp * 1000
|
||||
try {
|
||||
if (draggedFromBeginning){
|
||||
player.seekTo(startpos)
|
||||
}else{
|
||||
var endpos = (endTimestamp * 1000).toLong() - 1500
|
||||
if (endpos < (startTimestamp * 1000).toLong()) endpos = startpos
|
||||
var endpos = (endTimestamp * 1000) - 1500
|
||||
if (endpos < (startTimestamp * 1000)) endpos = startpos
|
||||
player.seekTo(endpos)
|
||||
}
|
||||
player.play()
|
||||
|
|
@ -466,7 +523,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
newCutBtn.setOnClickListener {
|
||||
cutSection.visibility = View.VISIBLE
|
||||
cutListSection.visibility = View.GONE
|
||||
rangeSlider.setValues(0F, timeSeconds.toFloat())
|
||||
rangeSlider.setValues(0F, ( itemDurationTimestamp / 1000).toFloat())
|
||||
player.seekTo(0)
|
||||
suggestedChips.children.apply {
|
||||
this.forEach {
|
||||
|
|
@ -495,8 +552,8 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
}
|
||||
|
||||
private fun createChip(timestamp: String) : Chip {
|
||||
val startTimestamp = convertStringToTimestamp(timestamp.split("-")[0].replace(";", ""))
|
||||
val endTimestamp = convertStringToTimestamp(timestamp.split("-")[1].replace(";", ""))
|
||||
val startTimestamp = timestamp.split("-")[0].replace(";", "").convertToTimestamp()
|
||||
val endTimestamp = timestamp.split("-")[1].replace(";", "").convertToTimestamp()
|
||||
|
||||
val chip = layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
|
||||
chip.text = timestamp
|
||||
|
|
@ -508,9 +565,9 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
|
||||
chip.setOnClickListener {
|
||||
if (chip.isChecked) {
|
||||
rangeSlider.setValues(startTimestamp.toFloat(), endTimestamp.toFloat())
|
||||
rangeSlider.setValues((startTimestamp / 1000).toFloat(), (endTimestamp / 1000).toFloat())
|
||||
player.prepare()
|
||||
player.seekTo((startTimestamp * 1000).toLong())
|
||||
player.seekTo(startTimestamp)
|
||||
player.play()
|
||||
}else {
|
||||
player.seekTo(0)
|
||||
|
|
@ -600,21 +657,95 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
|
|||
}
|
||||
}.flowOn(Dispatchers.Main)
|
||||
|
||||
private fun convertStringToTimestamp(duration: String): Int {
|
||||
return try {
|
||||
val timeArray = duration.split(":")
|
||||
var timeSeconds = timeArray[timeArray.lastIndex].toInt()
|
||||
var times = 60
|
||||
for (i in timeArray.lastIndex - 1 downTo 0) {
|
||||
timeSeconds += timeArray[i].toInt() * times
|
||||
times *= 60
|
||||
}
|
||||
timeSeconds
|
||||
}catch (e: Exception){
|
||||
e.printStackTrace()
|
||||
0
|
||||
}
|
||||
}
|
||||
// private fun showTimestampBottomSheet(startTimestamp: Boolean, currentTimestamp: Long, onChange: (value: Long) -> Unit){
|
||||
// val bottomSheet = BottomSheetDialog(requireContext())
|
||||
// bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
// bottomSheet.setContentView(R.layout.adjust_cut_timestamp)
|
||||
//
|
||||
// val hours = bottomSheet.findViewById<NumberPicker>(R.id.hours)!!
|
||||
// val minutes = bottomSheet.findViewById<NumberPicker>(R.id.minutes)!!
|
||||
// val seconds = bottomSheet.findViewById<NumberPicker>(R.id.seconds)!!
|
||||
// val milliseconds = bottomSheet.findViewById<NumberPicker>(R.id.milliseconds)!!
|
||||
//
|
||||
// val current = currentTimestamp.toTimePeriodsArray()
|
||||
//
|
||||
// val setLimits = fun() {
|
||||
// val fromPeriods = fromTextInput.text.toString().convertToTimestamp().toTimePeriodsArray()
|
||||
// val toPeriods = toTextInput.text.toString().convertToTimestamp().toTimePeriodsArray()
|
||||
// val totalPeriods = itemDurationTimestamp.toTimePeriodsArray()
|
||||
//
|
||||
// if (startTimestamp){
|
||||
// hours.minValue = 0
|
||||
// hours.maxValue = toPeriods[Extensions.Period.HOUR]!!
|
||||
//
|
||||
// minutes.minValue = 0
|
||||
// if (hours.maxValue > 0){
|
||||
// minutes.maxValue = 59
|
||||
// }else{
|
||||
// minutes.maxValue = toPeriods[Extensions.Period.MINUTE]!!
|
||||
// }
|
||||
//
|
||||
// seconds.minValue = 0
|
||||
// if (minutes.maxValue > 0){
|
||||
// seconds.maxValue = 59
|
||||
// }else{
|
||||
// seconds.maxValue = toPeriods[Extensions.Period.SECOND]!!
|
||||
// }
|
||||
// }else{
|
||||
// hours.minValue = fromPeriods[Extensions.Period.HOUR]!!
|
||||
// hours.maxValue = totalPeriods[Extensions.Period.HOUR]!!
|
||||
//
|
||||
// if (hours.minValue < 1){
|
||||
// minutes.minValue = fromPeriods[Extensions.Period.MINUTE]!!
|
||||
// minutes.maxValue = totalPeriods[Extensions.Period.MINUTE]!!
|
||||
// }else{
|
||||
// minutes.minValue = 0
|
||||
// minutes.maxValue = 59
|
||||
// }
|
||||
//
|
||||
// if (minutes.maxValue < 1){
|
||||
// seconds.minValue = fromPeriods[Extensions.Period.SECOND]!!
|
||||
// seconds.maxValue = totalPeriods[Extensions.Period.MINUTE]!!
|
||||
// }else{
|
||||
// seconds.minValue = 0
|
||||
// seconds.maxValue = 59
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// setLimits()
|
||||
// val timeSeconds = (itemDurationTimestamp / 1000).toInt()
|
||||
// hours.value = current[Extensions.Period.HOUR]!!
|
||||
// bottomSheet.findViewById<LinearLayout>(R.id.hours_container)?.isVisible = timeSeconds >= 3600
|
||||
//
|
||||
// minutes.value = current[Extensions.Period.MINUTE]!!
|
||||
// bottomSheet.findViewById<LinearLayout>(R.id.minutes_container)?.isVisible = timeSeconds >= 60
|
||||
//
|
||||
// seconds.value = current[Extensions.Period.SECOND]!!
|
||||
//
|
||||
// milliseconds.minValue = 0
|
||||
// milliseconds.maxValue = 9
|
||||
// milliseconds.value = current[Extensions.Period.MILLISECOND]!!
|
||||
//
|
||||
// val getTimeStamp = fun() : Long {
|
||||
// return "${hours.value}:${minutes.value}:${seconds.value}.${milliseconds.value}".convertToTimestamp()
|
||||
// }
|
||||
//
|
||||
// val items = listOf(
|
||||
// hours, minutes, seconds, milliseconds
|
||||
// )
|
||||
//
|
||||
// items.forEach {
|
||||
// it.setOnValueChangedListener { _, _, _ ->
|
||||
// setLimits()
|
||||
// onChange(getTimeStamp())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// bottomSheet.show()
|
||||
// bottomSheet.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
// }
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
super.onCancel(dialog)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.content.Intent
|
|||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextUtils
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
|
@ -17,6 +18,7 @@ import android.widget.LinearLayout
|
|||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.fragment.app.Fragment
|
||||
|
|
@ -45,6 +47,7 @@ import kotlinx.coroutines.SupervisorJob
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
class DownloadAudioFragment(private var resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "", private var nonSpecific: Boolean = false) : Fragment(), GUISync {
|
||||
|
|
|
|||
|
|
@ -553,10 +553,8 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
|
|||
}else{
|
||||
//open multi download card instead
|
||||
if (activity is ShareActivity){
|
||||
val preferredType = DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!)
|
||||
findNavController().navigate(R.id.action_downloadBottomSheetDialog_to_selectPlaylistItemsDialog, bundleOf(
|
||||
Pair("results", result),
|
||||
Pair("type", preferredType),
|
||||
Pair("resultIDs", result.map { it!!.id }.toLongArray()),
|
||||
))
|
||||
}else{
|
||||
dismiss()
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
|
|||
|
||||
preferences.edit().putString("lastCommandTemplateUsed", downloadItem.format.format_note).apply()
|
||||
|
||||
if (!Patterns.WEB_URL.matcher(downloadItem.url).matches()){
|
||||
if (!Patterns.WEB_URL.matcher(downloadItem.url).matches() && downloadItem.url.endsWith(".txt")){
|
||||
downloadItem.format = downloadViewModel.generateCommandFormat(CommandTemplate(0,"txt", "-a \"${downloadItem.url}\"", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false))
|
||||
downloadItem.url = ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,20 @@ package com.deniscerri.ytdl.ui.downloadcard
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.text.format.DateFormat
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
|
|
@ -21,7 +26,11 @@ import android.widget.Button
|
|||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.get
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
|
|
@ -36,6 +45,7 @@ import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
|
|||
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
|
||||
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
|
||||
import com.deniscerri.ytdl.services.ProcessDownloadsInBackgroundService
|
||||
import com.deniscerri.ytdl.ui.adapter.ConfigureMultipleDownloadsAdapter
|
||||
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
|
||||
import com.deniscerri.ytdl.util.FileUtil
|
||||
|
|
@ -49,12 +59,17 @@ import com.google.android.material.button.MaterialButton
|
|||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
|
@ -75,6 +90,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
private lateinit var sharedPreferences: SharedPreferences
|
||||
|
||||
private lateinit var currentDownloadIDs: List<Long>
|
||||
private var processingDownloadsJobID: Long = 0
|
||||
private var processingItemsCount : Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -86,6 +103,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
infoUtil = InfoUtil(requireContext())
|
||||
|
||||
currentDownloadIDs = arguments?.getLongArray("currentDownloadIDs")?.toList() ?: listOf()
|
||||
processingDownloadsJobID = arguments?.getLong("processingDownloadsJobID") ?: 0
|
||||
processingItemsCount = currentDownloadIDs.size
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi", "NotifyDataSetChanged")
|
||||
|
|
@ -131,40 +150,83 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
|
||||
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
|
||||
val download = view.findViewById<Button>(R.id.bottomsheet_download_button)
|
||||
val progressBar = view.findViewById<LinearProgressIndicator>(R.id.loadingItemsProgress)
|
||||
|
||||
filesize = view.findViewById(R.id.filesize)
|
||||
count = view.findViewById(R.id.count)
|
||||
|
||||
|
||||
scheduleBtn.setOnClickListener{
|
||||
UiUtil.showDatePicker(parentFragmentManager) { cal ->
|
||||
scheduleBtn.isEnabled = false
|
||||
download.isEnabled = false
|
||||
fun initSchedule() {
|
||||
UiUtil.showDatePicker(parentFragmentManager) { cal ->
|
||||
scheduleBtn.isEnabled = false
|
||||
download.isEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
downloadViewModel.deleteAllWithID(currentDownloadIDs)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
downloadViewModel.deleteAllWithID(currentDownloadIDs)
|
||||
downloadViewModel.downloadProcessingDownloads(cal.timeInMillis)
|
||||
val jobData = withContext(Dispatchers.IO){
|
||||
downloadViewModel.getLoadingProcessingDownloadJob(processingDownloadsJobID)
|
||||
}
|
||||
jobData?.job?.cancel(CancellationException())
|
||||
downloadProcessDownloadsInBackground(jobData, cal.timeInMillis)
|
||||
|
||||
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(cal.timeInMillis)
|
||||
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(cal.timeInMillis)
|
||||
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
if (progressBar.isVisible){
|
||||
val continueInBackgroundSnackBar = Snackbar.make(view, R.string.process_downloads_background, Snackbar.LENGTH_LONG)
|
||||
val snackbarView: View = continueInBackgroundSnackBar.view
|
||||
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
|
||||
snackTextView.maxLines = 9999999
|
||||
continueInBackgroundSnackBar.setAction(R.string.ok) {
|
||||
initSchedule()
|
||||
}
|
||||
continueInBackgroundSnackBar.show()
|
||||
}else{
|
||||
initSchedule()
|
||||
}
|
||||
}
|
||||
|
||||
download!!.setOnClickListener {
|
||||
scheduleBtn.isEnabled = false
|
||||
download.isEnabled = false
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
downloadViewModel.deleteAllWithID(currentDownloadIDs)
|
||||
downloadViewModel.downloadProcessingDownloads()
|
||||
fun initDownload() {
|
||||
scheduleBtn.isEnabled = false
|
||||
download.isEnabled = false
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
downloadViewModel.deleteAllWithID(currentDownloadIDs)
|
||||
}
|
||||
|
||||
val jobData = withContext(Dispatchers.IO){
|
||||
downloadViewModel.getLoadingProcessingDownloadJob(processingDownloadsJobID)
|
||||
}
|
||||
jobData?.job?.cancel(CancellationException())
|
||||
downloadProcessDownloadsInBackground(jobData)
|
||||
dismiss()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
if (progressBar.isVisible){
|
||||
val continueInBackgroundSnackBar = Snackbar.make(view, R.string.process_downloads_background, Snackbar.LENGTH_LONG)
|
||||
val snackbarView: View = continueInBackgroundSnackBar.view
|
||||
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
|
||||
snackTextView.maxLines = 9999999
|
||||
continueInBackgroundSnackBar.setAction(R.string.ok) {
|
||||
initDownload()
|
||||
}
|
||||
continueInBackgroundSnackBar.show()
|
||||
}else{
|
||||
initDownload()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
download.setOnLongClickListener {
|
||||
|
|
@ -206,8 +268,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
withContext(Dispatchers.IO){
|
||||
downloadViewModel.continueUpdatingFormatsOnBackground()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -537,45 +599,55 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
downloadViewModel.processingDownloads.collectLatest {
|
||||
count.text = "${it.size} ${getString(R.string.selected)}"
|
||||
listAdapter.submitList(it)
|
||||
withContext(Dispatchers.Main){
|
||||
updateFileSize(it.map { it2 -> it2.format.filesize })
|
||||
}
|
||||
downloadViewModel.loadingProcessingDownloadsJobs.collectLatest { jobs ->
|
||||
processingItemsCount = jobs.firstOrNull { j -> j.jobID == processingDownloadsJobID }?.itemIDs?.size ?: currentDownloadIDs.size
|
||||
lifecycleScope.launch {
|
||||
downloadViewModel.processingDownloads.collectLatest {
|
||||
count.text = "${it.size} ${getString(R.string.selected)}"
|
||||
|
||||
if (it.isNotEmpty()){
|
||||
if (it.all { it2 -> it2.type == it[0].type }) {
|
||||
withContext(Dispatchers.Main) {
|
||||
bottomAppBar.menu[1].icon?.alpha = 255
|
||||
if (it[0].type != DownloadViewModel.Type.command) {
|
||||
bottomAppBar.menu[3].icon?.alpha = 255
|
||||
val loading = it.size != processingItemsCount
|
||||
progressBar.isVisible = loading
|
||||
bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading }
|
||||
|
||||
listAdapter.submitList(it)
|
||||
withContext(Dispatchers.Main){
|
||||
updateFileSize(it.map { it2 -> it2.format.filesize })
|
||||
}
|
||||
|
||||
if (it.isNotEmpty()){
|
||||
if (it.all { it2 -> it2.type == it[0].type }) {
|
||||
withContext(Dispatchers.Main) {
|
||||
bottomAppBar.menu[1].icon?.alpha = 255
|
||||
if (it[0].type != DownloadViewModel.Type.command) {
|
||||
bottomAppBar.menu[3].icon?.alpha = 255
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bottomAppBar.menu[1].icon?.alpha = 30
|
||||
bottomAppBar.menu[3].icon?.alpha = 30
|
||||
}
|
||||
|
||||
val type = it.first().type
|
||||
|
||||
when(type){
|
||||
DownloadViewModel.Type.audio -> {
|
||||
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
|
||||
}
|
||||
DownloadViewModel.Type.video -> {
|
||||
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
|
||||
|
||||
}
|
||||
DownloadViewModel.Type.command -> {
|
||||
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
bottomAppBar.menu[1].icon?.alpha = 30
|
||||
bottomAppBar.menu[3].icon?.alpha = 30
|
||||
|
||||
}
|
||||
|
||||
val type = it.first().type
|
||||
|
||||
when(type){
|
||||
DownloadViewModel.Type.audio -> {
|
||||
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
|
||||
}
|
||||
DownloadViewModel.Type.video -> {
|
||||
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
|
||||
|
||||
}
|
||||
DownloadViewModel.Type.command -> {
|
||||
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -700,15 +772,14 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
|
||||
UiUtil.showGenericDeleteDialog(requireContext(), deletedItem.title){
|
||||
lifecycleScope.launch {
|
||||
val count = withContext(Dispatchers.IO){
|
||||
downloadViewModel.getProcessingDownloadsCount()
|
||||
}
|
||||
processingItemsCount--
|
||||
downloadViewModel.deleteDownload(id)
|
||||
|
||||
if (count > 1){
|
||||
if (processingItemsCount > 0){
|
||||
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
|
||||
.setAction(getString(R.string.undo)) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
processingItemsCount++
|
||||
downloadViewModel.insert(deletedItem)
|
||||
}
|
||||
}.show()
|
||||
|
|
@ -736,9 +807,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
requireActivity().lifecycleScope.launch {
|
||||
downloadViewModel.deleteProcessing()
|
||||
}
|
||||
downloadViewModel.cancelLoadingProcessingDownloads(processingDownloadsJobID)
|
||||
downloadViewModel.deleteProcessing()
|
||||
super.onDismiss(dialog)
|
||||
}
|
||||
|
||||
|
|
@ -757,16 +827,16 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
val deletedItem = withContext(Dispatchers.IO){
|
||||
downloadViewModel.getItemByID(itemID)
|
||||
}
|
||||
val count = withContext(Dispatchers.IO){
|
||||
downloadViewModel.getProcessingDownloadsCount()
|
||||
}
|
||||
processingItemsCount--
|
||||
withContext(Dispatchers.IO){
|
||||
downloadViewModel.deleteDownload(deletedItem.id)
|
||||
}
|
||||
|
||||
if (count > 1) {
|
||||
|
||||
if (processingItemsCount > 0) {
|
||||
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
|
||||
.setAction(getString(R.string.undo)) {
|
||||
processingItemsCount++
|
||||
downloadViewModel.insert(deletedItem)
|
||||
}.show()
|
||||
}else{
|
||||
|
|
@ -819,5 +889,17 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
|
|||
}
|
||||
}
|
||||
|
||||
private fun downloadProcessDownloadsInBackground(jobData: DownloadViewModel.ProcessingItemsJob?, timeInMillis: Long = 0){
|
||||
Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent ->
|
||||
jobData?.apply {
|
||||
intent.putExtra("itemType", jobData.itemType)
|
||||
intent.putExtra("itemIDs", jobData.itemIDs.toLongArray())
|
||||
intent.putExtra("timeInMillis", timeInMillis)
|
||||
}
|
||||
|
||||
requireActivity().startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -253,13 +253,14 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
|
|||
.sumOf { itt -> itt.filesize }
|
||||
}
|
||||
}
|
||||
hasGenericFormats = false
|
||||
hasGenericFormats = formatCollection.size == 0
|
||||
withContext(Dispatchers.Main){
|
||||
shimmers.visibility = View.GONE
|
||||
shimmers.stopShimmer()
|
||||
filterBtn.isVisible = true
|
||||
filterBtn.isVisible = !hasGenericFormats
|
||||
addFormatsToView()
|
||||
refreshBtn.visibility = View.GONE
|
||||
refreshBtn.isVisible = hasGenericFormats
|
||||
refreshBtn.isEnabled = hasGenericFormats
|
||||
formatListLinearLayout.visibility = View.VISIBLE
|
||||
}
|
||||
}catch (e: Exception){
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import com.google.android.material.textfield.TextInputLayout
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.hamcrest.core.Every
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Month
|
||||
import java.util.Calendar
|
||||
|
|
@ -84,6 +85,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
|
|||
private lateinit var title: TextInputLayout
|
||||
private lateinit var url: TextInputLayout
|
||||
private lateinit var everyCat: AutoCompleteTextView
|
||||
private lateinit var categoryAdapter: ArrayAdapter<String>
|
||||
private lateinit var everyTime: TextInputLayout
|
||||
private lateinit var weekDays: ChipGroup
|
||||
private lateinit var everyMonthDay: TextInputLayout
|
||||
|
|
@ -291,7 +293,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
|
|||
everyNr.editText?.doAfterTextChanged { checkIfValid() }
|
||||
|
||||
val cats = ObserveSourcesRepository.everyCategoryName.map { getString(it.value) }
|
||||
val adapter = ArrayAdapter(requireActivity(),android.R.layout.simple_dropdown_item_1line, cats)
|
||||
categoryAdapter = ArrayAdapter(requireActivity(),android.R.layout.simple_dropdown_item_1line, cats)
|
||||
everyCat.doAfterTextChanged {
|
||||
everyTime.isVisible = it.toString() != cats[0]
|
||||
weekDays.isVisible = it.toString() == cats[2]
|
||||
|
|
@ -299,7 +301,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
|
|||
startTime.isVisible = !startMonth.isVisible
|
||||
everyMonthDay.isVisible = startMonth.isVisible
|
||||
}
|
||||
everyCat.setAdapter(adapter)
|
||||
everyCat.setAdapter(categoryAdapter)
|
||||
if (currentItem != null){
|
||||
val idx = ObserveSourcesRepository.EveryCategory.values().indexOf(currentItem!!.everyCategory)
|
||||
everyCat.setText(cats[idx], false)
|
||||
|
|
@ -473,6 +475,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
|
|||
okButton.setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
val item: DownloadItem = getDownloadItem()
|
||||
item.url = url.editText!!.text.toString()
|
||||
|
||||
var ends = 0L
|
||||
if(endsOn.isChecked){
|
||||
|
|
@ -484,7 +487,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
|
|||
endsAfterCount = endsAfterNr.editText!!.text.toString().toInt()
|
||||
}
|
||||
|
||||
val category = ObserveSourcesRepository.EveryCategory.values()[adapter.getPosition(everyCat.text.toString())]
|
||||
val category = ObserveSourcesRepository.EveryCategory.values()[categoryAdapter.getPosition(everyCat.text.toString())]
|
||||
|
||||
val observeItem = ObserveSourcesItem(
|
||||
id = currentItem?.id ?: 0,
|
||||
|
|
@ -493,7 +496,7 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
|
|||
downloadItemTemplate = item,
|
||||
everyNr = everyNr.editText!!.text.toString().toInt(),
|
||||
everyCategory = category,
|
||||
everyTime = everyTime.tag as Long,
|
||||
everyTime = (everyTime.tag ?: 0L) as Long,
|
||||
weeklyConfig = if (category == ObserveSourcesRepository.EveryCategory.WEEK) {
|
||||
ObserveSourcesWeeklyConfig(
|
||||
weekDays = weekDays.children.filter { (it as Chip).isChecked }.map { it.tag.toString().toInt() }.toList()
|
||||
|
|
@ -534,16 +537,31 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
|
|||
}
|
||||
|
||||
private fun checkIfValid(){
|
||||
var valid = title.editText?.text?.isNotBlank() == true
|
||||
&& url.editText?.text?.isNotBlank() == true
|
||||
&& everyNr.editText?.text?.isNotBlank() == true
|
||||
&& everyTime.editText?.text?.isNotBlank() == true
|
||||
&& startTime.editText?.text?.isNotBlank() == true
|
||||
val title = title.editText?.text?.isNotBlank() == true
|
||||
val url = url.editText?.text?.isNotBlank() == true
|
||||
val category = ObserveSourcesRepository.EveryCategory.values()[categoryAdapter.getPosition(everyCat.text.toString())]
|
||||
|
||||
if (endsOn.isChecked && endsOnTime.editText?.text?.isBlank() == true) valid = false
|
||||
else if (endsAfter.isChecked && endsAfterNr.editText?.text?.isBlank() == true) valid = false
|
||||
val everyNr = everyNr.editText?.text?.isNotBlank() == true
|
||||
val everyTime = everyTime.editText?.text?.isNotBlank() == true
|
||||
|
||||
okButton.isEnabled = valid
|
||||
val every = when(category){
|
||||
ObserveSourcesRepository.EveryCategory.HOUR -> {
|
||||
everyNr
|
||||
}
|
||||
else -> {
|
||||
everyNr && everyTime
|
||||
}
|
||||
}
|
||||
|
||||
val ends = if (endsOn.isChecked){
|
||||
endsOnTime.editText?.text?.isNotBlank() == true
|
||||
}else if (endsAfter.isChecked){
|
||||
endsAfterNr.editText?.text?.isNotBlank() == true
|
||||
}else{
|
||||
true
|
||||
}
|
||||
|
||||
okButton.isEnabled = title && url && every && ends
|
||||
}
|
||||
|
||||
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.map
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
|
@ -31,6 +34,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -49,28 +53,23 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
|
|||
private lateinit var count: TextView
|
||||
private lateinit var selectBetween: MenuItem
|
||||
|
||||
private lateinit var items: List<ResultItem?>
|
||||
private lateinit var resultItemIDs: List<Long>
|
||||
private lateinit var itemURLs: List<String>
|
||||
private lateinit var type: DownloadViewModel.Type
|
||||
private var items = listOf<ResultItem>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
|
||||
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33){
|
||||
arguments?.getParcelableArrayList("results", ResultItem::class.java)
|
||||
}else{
|
||||
arguments?.getParcelableArrayList<ResultItem>("results")
|
||||
}.apply {
|
||||
arguments?.getLongArray("resultIDs").apply {
|
||||
if (this == null){
|
||||
dismiss()
|
||||
return
|
||||
}else{
|
||||
items = this
|
||||
resultItemIDs = this.toList()
|
||||
}
|
||||
}
|
||||
type = arguments?.getSerializable("type") as DownloadViewModel.Type
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi", "NotifyDataSetChanged")
|
||||
|
|
@ -90,6 +89,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
|
|||
}
|
||||
}
|
||||
|
||||
val progress = view.findViewById<LinearProgressIndicator>(R.id.loadingItemsProgress)
|
||||
|
||||
listAdapter =
|
||||
PlaylistAdapter(
|
||||
|
|
@ -101,7 +101,6 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
|
|||
recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
recyclerView.adapter = listAdapter
|
||||
recyclerView.enableFastScroll()
|
||||
listAdapter.submitList(items)
|
||||
|
||||
count = view.findViewById(R.id.count)
|
||||
count.text = "0 ${resources.getString(R.string.selected)}"
|
||||
|
|
@ -159,7 +158,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
|
|||
fromTextInput.isEnabled = true
|
||||
toTextInput.isEnabled = true
|
||||
ok.isEnabled = true
|
||||
count.text = resources.getString(R.string.all_items_selected)
|
||||
count.text = "(${listAdapter.getCheckedItems().size}) ${resources.getString(R.string.all_items_selected)} "
|
||||
}else{
|
||||
reset()
|
||||
fromTextInput.isEnabled = true
|
||||
|
|
@ -183,25 +182,15 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
|
|||
withContext(Dispatchers.Main){
|
||||
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf(
|
||||
Pair("result", resultItem),
|
||||
Pair("type", downloadViewModel.getDownloadType(type, resultItem.url)),
|
||||
Pair("type", downloadViewModel.getDownloadType(url = resultItem.url)),
|
||||
))
|
||||
}
|
||||
}else{
|
||||
val downloadItems = mutableListOf<DownloadItem>()
|
||||
checkedResultItems.forEach { c ->
|
||||
c!!.id = 0
|
||||
val i = downloadViewModel.createDownloadItemFromResult(
|
||||
result = c, givenType = type)
|
||||
if (type == DownloadViewModel.Type.command){
|
||||
i.format = downloadViewModel.getLatestCommandTemplateAsFormat()
|
||||
}
|
||||
downloadItems.add(i)
|
||||
}
|
||||
|
||||
downloadViewModel.insertToProcessing(downloadItems)
|
||||
|
||||
val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(checkedResultItems.map { it!!.id })
|
||||
withContext(Dispatchers.Main){
|
||||
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadMultipleBottomSheetDialog)
|
||||
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadMultipleBottomSheetDialog,
|
||||
bundleOf(Pair("processingDownloadsJobID", jobID))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -218,7 +207,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
|
|||
listAdapter.invertSelected(items)
|
||||
val checkedItems = listAdapter.getCheckedItems()
|
||||
if (checkedItems.size == items.size){
|
||||
count.text = resources.getString(R.string.all_items_selected)
|
||||
count.text = "(${listAdapter.getCheckedItems().size}) ${resources.getString(R.string.all_items_selected)} "
|
||||
}else{
|
||||
count.text = "${checkedItems.size} ${resources.getString(R.string.selected)}"
|
||||
}
|
||||
|
|
@ -245,7 +234,23 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
|
|||
}
|
||||
|
||||
selectBetween = bottomAppBar.menu.findItem(R.id.select_between)
|
||||
itemURLs = items.map { it!!.url }
|
||||
|
||||
lifecycleScope.launch {
|
||||
resultViewModel.items.map { items -> items.filter { resultItemIDs.contains(it.id) } }.observe(this@SelectPlaylistItemsDialog) {
|
||||
val isLoading = it.size != resultItemIDs.size
|
||||
progress.isVisible = isLoading
|
||||
listAdapter.submitList(it)
|
||||
recyclerView.suppressLayout(isLoading)
|
||||
bottomAppBar.menu.children.forEach { c -> c.isEnabled = !isLoading }
|
||||
ok.isEnabled = !isLoading
|
||||
checkAll.isEnabled = !isLoading
|
||||
fromTextInput.isEnabled = !isLoading
|
||||
toTextInput.isEnabled = !isLoading
|
||||
|
||||
items = it
|
||||
itemURLs = items.map { itm -> itm.url }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +268,7 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
|
|||
|
||||
override fun onCardSelect(itemURL: String, isChecked: Boolean, checkedItems: List<String>) {
|
||||
if (checkedItems.size == items.size){
|
||||
count.text = resources.getString(R.string.all_items_selected)
|
||||
count.text = "(${listAdapter.getCheckedItems().size}) ${resources.getString(R.string.all_items_selected)} "
|
||||
}else{
|
||||
count.text = "${checkedItems.size} ${resources.getString(R.string.selected)}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ import com.google.android.material.color.MaterialColors
|
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
@ -251,19 +253,18 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
|
|||
R.id.redownload -> {
|
||||
lifecycleScope.launch {
|
||||
val selectedObjects = getSelectedIDs()
|
||||
if (preferences.getBoolean("download_card", true)){
|
||||
withContext(Dispatchers.IO){
|
||||
downloadViewModel.addDownloadsToProcessing(selectedObjects)
|
||||
val showDownloadCard = preferences.getBoolean("download_card", true)
|
||||
if (showDownloadCard) {
|
||||
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
|
||||
downloadViewModel.turnDownloadItemsToProcessingDownloads(selectedObjects)
|
||||
}
|
||||
withContext(Dispatchers.Main){
|
||||
val bundle = Bundle()
|
||||
bundle.putLongArray("currentDownloadIDs", selectedObjects.toLongArray())
|
||||
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2,bundle)
|
||||
}
|
||||
}else{
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadViewModel.reQueueDownloadItems(selectedObjects)
|
||||
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundle)
|
||||
}
|
||||
}else {
|
||||
downloadViewModel.reQueueDownloadItems(selectedObjects)
|
||||
}
|
||||
|
||||
adapter.clearCheckedItems()
|
||||
|
|
@ -275,7 +276,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
|
|||
}
|
||||
R.id.select_all -> {
|
||||
adapter.checkAll()
|
||||
mode?.title = getString(R.string.all_items_selected)
|
||||
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
|
||||
true
|
||||
}
|
||||
R.id.invert_selected -> {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
package com.deniscerri.ytdl.ui.downloads
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
|
|
@ -10,9 +15,11 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
|
|
@ -22,6 +29,7 @@ import com.deniscerri.ytdl.MainActivity
|
|||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.database.repository.DownloadRepository
|
||||
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdl.services.ProcessDownloadsInBackgroundService
|
||||
import com.deniscerri.ytdl.util.Extensions.createBadge
|
||||
import com.deniscerri.ytdl.util.NotificationUtil
|
||||
import com.deniscerri.ytdl.util.UiUtil
|
||||
|
|
@ -116,9 +124,24 @@ class DownloadQueueMainFragment : Fragment(){
|
|||
initMenu()
|
||||
|
||||
if (arguments?.getString("tab") != null){
|
||||
tabLayout.getTabAt(3)!!.select()
|
||||
tabLayout.getTabAt(4)!!.select()
|
||||
viewPager2.postDelayed( {
|
||||
viewPager2.setCurrentItem(3, false)
|
||||
viewPager2.setCurrentItem(4, false)
|
||||
val reconfigureID = arguments?.getLong("reconfigure")
|
||||
reconfigureID?.apply {
|
||||
lifecycleScope.launch {
|
||||
val item = withContext(Dispatchers.IO){
|
||||
downloadViewModel.getItemByID(reconfigureID)
|
||||
}
|
||||
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
|
||||
Pair("downloadItem", item),
|
||||
Pair("result", downloadViewModel.createResultItemFromDownload(item)),
|
||||
Pair("type", item.type)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
|
||||
|
|
@ -243,14 +266,37 @@ class DownloadQueueMainFragment : Fragment(){
|
|||
workManager.cancelAllWorkByTag("download")
|
||||
lifecycleScope.launch {
|
||||
val notificationUtil = NotificationUtil(requireContext())
|
||||
downloadViewModel.cancelActiveQueued()
|
||||
cancelBackgroundProcessingDownloads()
|
||||
val activeAndQueued = withContext(Dispatchers.IO){
|
||||
downloadViewModel.getActiveAndQueuedDownloadIDs()
|
||||
}
|
||||
downloadViewModel.cancelActiveQueued()
|
||||
activeAndQueued.forEach { id ->
|
||||
YoutubeDL.getInstance().destroyProcessById(id.toString())
|
||||
notificationUtil.cancelDownloadNotification(id.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelBackgroundProcessingDownloads(){
|
||||
val connection = object : ServiceConnection {
|
||||
private lateinit var mService: ProcessDownloadsInBackgroundService
|
||||
private var mBound: Boolean = false
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
val binder = service as ProcessDownloadsInBackgroundService.LocalBinder
|
||||
mService = binder.service
|
||||
mBound = true
|
||||
|
||||
mService.cancelAllProcessingJobs()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg0: ComponentName) {
|
||||
mBound = false
|
||||
}
|
||||
}
|
||||
|
||||
Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent ->
|
||||
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,9 @@ import com.google.android.material.color.MaterialColors
|
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
@ -243,20 +245,18 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
|
|||
R.id.redownload -> {
|
||||
lifecycleScope.launch {
|
||||
val selectedObjects = getSelectedIDs()
|
||||
|
||||
if (preferences.getBoolean("download_card", true)){
|
||||
withContext(Dispatchers.IO){
|
||||
downloadViewModel.addDownloadsToProcessing(selectedObjects)
|
||||
val showDownloadCard = preferences.getBoolean("download_card", true)
|
||||
if (showDownloadCard) {
|
||||
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
|
||||
downloadViewModel.turnDownloadItemsToProcessingDownloads(selectedObjects)
|
||||
}
|
||||
withContext(Dispatchers.Main){
|
||||
val bundle = Bundle()
|
||||
bundle.putLongArray("currentDownloadIDs", selectedObjects.toLongArray())
|
||||
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2,bundle)
|
||||
}
|
||||
}else{
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadViewModel.reQueueDownloadItems(selectedObjects)
|
||||
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundle)
|
||||
}
|
||||
}else {
|
||||
downloadViewModel.reQueueDownloadItems(selectedObjects)
|
||||
}
|
||||
|
||||
adapter.clearCheckedItems()
|
||||
|
|
@ -266,7 +266,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
|
|||
}
|
||||
R.id.select_all -> {
|
||||
adapter.checkAll()
|
||||
mode?.title = getString(R.string.all_items_selected)
|
||||
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
|
||||
true
|
||||
}
|
||||
R.id.invert_selected -> {
|
||||
|
|
|
|||
|
|
@ -183,22 +183,14 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
|
|||
|
||||
lifecycleScope.launch{
|
||||
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
|
||||
if (res.downloadItems.isNotEmpty() || res.historyItems.isNotEmpty()) {
|
||||
if (res.isNotEmpty()){
|
||||
withContext(Dispatchers.Main){
|
||||
kotlin.runCatching {
|
||||
UiUtil.handleExistingDownloadsResponse(
|
||||
requireActivity(),
|
||||
requireActivity().lifecycleScope,
|
||||
requireActivity().supportFragmentManager,
|
||||
res,
|
||||
downloadViewModel,
|
||||
historyViewModel)
|
||||
}
|
||||
val bundle = bundleOf(
|
||||
Pair("duplicates", res)
|
||||
)
|
||||
findNavController().navigate(R.id.action_historyFragment_to_downloadsAlreadyExistDialog, bundle)
|
||||
}
|
||||
downloadViewModel.alreadyExistsUiState.value = DownloadViewModel.AlreadyExistsUIState(
|
||||
mutableListOf(),
|
||||
mutableListOf()
|
||||
)
|
||||
downloadViewModel.alreadyExistsUiState.value = mutableListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -558,7 +550,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
|
|||
historyAdapter?.checkAll(historyList)
|
||||
selectedObjects.clear()
|
||||
historyList?.forEach { selectedObjects.add(it!!) }
|
||||
mode?.title = getString(R.string.all_items_selected)
|
||||
mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}"
|
||||
true
|
||||
}
|
||||
R.id.invert_selected -> {
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
|
|||
}
|
||||
R.id.select_all -> {
|
||||
adapter.checkAll()
|
||||
mode?.title = getString(R.string.all_items_selected)
|
||||
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
|
||||
true
|
||||
}
|
||||
R.id.invert_selected -> {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ import com.google.android.material.color.MaterialColors
|
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
@ -240,19 +242,18 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
|
|||
lifecycleScope.launch {
|
||||
val selectedObjects = getSelectedIDs()
|
||||
adapter.clearCheckedItems()
|
||||
if (preferences.getBoolean("download_card", true)){
|
||||
withContext(Dispatchers.IO){
|
||||
downloadViewModel.addDownloadsToProcessing(selectedObjects)
|
||||
val showDownloadCard = preferences.getBoolean("download_card", true)
|
||||
if (showDownloadCard) {
|
||||
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
|
||||
downloadViewModel.turnDownloadItemsToProcessingDownloads(selectedObjects)
|
||||
}
|
||||
withContext(Dispatchers.Main){
|
||||
val bundle = Bundle()
|
||||
bundle.putLongArray("currentDownloadIDs", selectedObjects.toLongArray())
|
||||
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2,bundle)
|
||||
}
|
||||
}else{
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadViewModel.reQueueDownloadItems(selectedObjects)
|
||||
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundle)
|
||||
}
|
||||
}else {
|
||||
downloadViewModel.reQueueDownloadItems(selectedObjects)
|
||||
}
|
||||
actionMode?.finish()
|
||||
|
||||
|
|
@ -261,7 +262,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
|
|||
}
|
||||
R.id.select_all -> {
|
||||
adapter.checkAll()
|
||||
mode?.title = getString(R.string.all_items_selected)
|
||||
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
|
||||
true
|
||||
}
|
||||
R.id.invert_selected -> {
|
||||
|
|
|
|||
|
|
@ -116,8 +116,7 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
|
|||
downloadViewModel.getItemByID(itemID)
|
||||
}
|
||||
withContext(Dispatchers.IO){
|
||||
item.downloadStartTime = 0
|
||||
downloadViewModel.queueDownloads(listOf(item), true)
|
||||
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(listOf(item.id))
|
||||
}
|
||||
}.onFailure {
|
||||
Toast.makeText(requireContext(), it.message, Toast.LENGTH_LONG).show()
|
||||
|
|
@ -125,7 +124,7 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
|
|||
}
|
||||
}
|
||||
|
||||
override fun onCardClick(itemID: Long) {
|
||||
override fun onCardClick(itemID: Long, position: Int) {
|
||||
lifecycleScope.launch {
|
||||
val item = withContext(Dispatchers.IO){
|
||||
downloadViewModel.getItemByID(itemID)
|
||||
|
|
@ -163,6 +162,7 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
|
|||
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(listOf(downloadItem))
|
||||
adapter.notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -221,7 +221,7 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
|
|||
|
||||
private val contextualActionBar = object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
mode!!.menuInflater.inflate(R.menu.queued_menu_context, menu)
|
||||
mode!!.menuInflater.inflate(R.menu.scheduled_downloads_menu_context, menu)
|
||||
mode.title = "${adapter.getSelectedObjectsCount(totalSize)} ${getString(R.string.selected)}"
|
||||
return true
|
||||
}
|
||||
|
|
@ -284,7 +284,7 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
|
|||
}
|
||||
R.id.select_all -> {
|
||||
adapter.checkAll()
|
||||
mode?.title = getString(R.string.all_items_selected)
|
||||
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
|
||||
true
|
||||
}
|
||||
R.id.invert_selected -> {
|
||||
|
|
@ -383,13 +383,13 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
|
|||
R.attr.colorOnSurfaceInverse,Color.TRANSPARENT
|
||||
)
|
||||
)
|
||||
.addSwipeRightActionIcon(R.drawable.ic_refresh)
|
||||
.addSwipeRightActionIcon(R.drawable.ic_downloads)
|
||||
.create()
|
||||
.decorate()
|
||||
super.onChildDraw(
|
||||
c,
|
||||
recyclerView,
|
||||
viewHolder!!,
|
||||
viewHolder,
|
||||
dX,
|
||||
dY,
|
||||
actionState,
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
|
|||
templatesAdapter.checkAll(templatesList)
|
||||
selectedObjects.clear()
|
||||
templatesList.forEach { selectedObjects.add(it) }
|
||||
mode?.title = getString(R.string.all_items_selected)
|
||||
mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}"
|
||||
true
|
||||
}
|
||||
R.id.invert_selected -> {
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
|
|||
downloadLogAdapter.checkAll(items)
|
||||
selectedObjects.clear()
|
||||
items.forEach { selectedObjects.add(it) }
|
||||
mode?.title = getString(R.string.all_items_selected)
|
||||
mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}"
|
||||
true
|
||||
}
|
||||
R.id.invert_selected -> {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.deniscerri.ytdl.ui.more.settings
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
|
|
@ -13,11 +16,10 @@ import androidx.work.ExistingWorkPolicy
|
|||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.util.FileUtil
|
||||
import com.deniscerri.ytdl.util.UiUtil
|
||||
import com.deniscerri.ytdl.work.AlarmScheduler
|
||||
import com.deniscerri.ytdl.work.DownloadWorker
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
|
@ -25,6 +27,8 @@ import java.util.concurrent.TimeUnit
|
|||
class DownloadSettingsFragment : BaseSettingsFragment() {
|
||||
override val title: Int = R.string.downloads
|
||||
|
||||
private lateinit var archivePath: Preference
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.downloading_preferences, rootKey)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
|
@ -47,6 +51,25 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
|
|||
true
|
||||
}
|
||||
|
||||
val preventDuplicateDownloads = findPreference<ListPreference>("prevent_duplicate_downloads")
|
||||
preventDuplicateDownloads?.setOnPreferenceChangeListener { _, newValue ->
|
||||
archivePath.isVisible = newValue == "download_archive"
|
||||
true
|
||||
}
|
||||
|
||||
archivePath = findPreference("download_archive_path")!!
|
||||
archivePath.summary = FileUtil.getDownloadArchivePath(requireContext())
|
||||
archivePath.isVisible = preferences.getString("prevent_duplicate_downloads", "") == "download_archive"
|
||||
archivePath.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
archivePathResultLauncher.launch(intent)
|
||||
true
|
||||
}
|
||||
|
||||
val useScheduler = findPreference<SwitchPreferenceCompat>("use_scheduler")
|
||||
val scheduleStart = findPreference<Preference>("schedule_start")
|
||||
scheduleStart?.summary = preferences.getString("schedule_start", "00:00")
|
||||
|
|
@ -55,8 +78,17 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
|
|||
val scheduler = AlarmScheduler(requireContext())
|
||||
|
||||
useScheduler?.setOnPreferenceChangeListener { preference, newValue ->
|
||||
var allowChange = true
|
||||
if (newValue as Boolean){
|
||||
scheduler.schedule()
|
||||
if (!scheduler.canSchedule() && Build.VERSION.SDK_INT >= 31){
|
||||
Intent().also { intent ->
|
||||
intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
|
||||
requireContext().startActivity(intent)
|
||||
}
|
||||
allowChange = false
|
||||
}else{
|
||||
scheduler.schedule()
|
||||
}
|
||||
}else{
|
||||
scheduler.cancel()
|
||||
//start worker if there are leftover downloads waiting for scheduler
|
||||
|
|
@ -72,7 +104,7 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
|
|||
workRequest.build()
|
||||
)
|
||||
}
|
||||
true
|
||||
allowChange
|
||||
}
|
||||
|
||||
scheduleStart?.setOnPreferenceClickListener {
|
||||
|
|
@ -102,4 +134,25 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private var archivePathResultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
result.data?.data?.let {
|
||||
activity?.contentResolver?.takePersistableUriPermission(
|
||||
it,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
}
|
||||
|
||||
val path = result.data!!.data.toString()
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val editor = preferences.edit()
|
||||
editor.putString("download_archive_path", path)
|
||||
editor.apply()
|
||||
archivePath.summary = FileUtil.getDownloadArchivePath(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,23 +1,37 @@
|
|||
package com.deniscerri.ytdl.ui.more.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
|
||||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.ui.adapter.SortableTextItemAdapter
|
||||
import com.deniscerri.ytdl.util.UiUtil
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
|
||||
class ProcessingSettingsFragment : BaseSettingsFragment() {
|
||||
override val title: Int = R.string.processing
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.processing_preferences, rootKey)
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity())
|
||||
val editor = prefs.edit()
|
||||
|
||||
val preferredFormatID : EditTextPreference? = findPreference("format_id")
|
||||
val preferredFormatIDAudio : EditTextPreference? = findPreference("format_id_audio")
|
||||
val subtitleLanguages : Preference? = findPreference("subs_lang")
|
||||
val formatImportanceAudio: Preference? = findPreference("format_importance_audio")
|
||||
val formatImportanceVideo: Preference? = findPreference("format_importance_video")
|
||||
|
||||
preferredFormatID?.title = "${getString(R.string.preferred_format_id)} [${getString(R.string.video)}]"
|
||||
preferredFormatID?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
|
||||
|
|
@ -34,6 +48,119 @@ class ProcessingSettingsFragment : BaseSettingsFragment() {
|
|||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
formatImportanceAudio?.apply {
|
||||
val items = requireContext().getStringArray(R.array.format_importance_audio)
|
||||
val itemValues = requireContext().getStringArray(R.array.format_importance_audio_values).toSet()
|
||||
val prefVideo = prefs.getString("format_importance_audio", itemValues.joinToString(","))!!
|
||||
summary = prefVideo.split(",").mapIndexed { index, s -> "${index + 1}. ${items[itemValues.indexOf(s)]}" }.joinToString("\n")
|
||||
|
||||
setOnPreferenceClickListener {
|
||||
val pref = prefs.getString("format_importance_audio", itemValues.joinToString(","))!!
|
||||
val itms = pref.split(",").map {
|
||||
Pair(it, items[itemValues.indexOf(it)])
|
||||
}.toMutableList()
|
||||
|
||||
showFormatImportanceDialog(getString(R.string.format_importance_audio), itms) { new ->
|
||||
editor.putString("format_importance_audio", new.joinToString(",") { it.first }).apply()
|
||||
formatImportanceAudio.summary = new.map { it.second }.mapIndexed { index, s -> "${index + 1}. $s" }.joinToString("\n")
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
formatImportanceVideo?.apply {
|
||||
val items = requireContext().getStringArray(R.array.format_importance_video)
|
||||
val itemValues = requireContext().getStringArray(R.array.format_importance_video_values).toSet()
|
||||
val prefVideo = prefs.getString("format_importance_video", itemValues.joinToString(","))!!
|
||||
summary = prefVideo.split(",").mapIndexed { index, s -> "${index + 1}. ${items[itemValues.indexOf(s)]}" }.joinToString("\n")
|
||||
|
||||
setOnPreferenceClickListener {
|
||||
val pref = prefs.getString("format_importance_video", itemValues.joinToString(","))!!
|
||||
val itms = pref.split(",").map {
|
||||
Pair(it, items[itemValues.indexOf(it)])
|
||||
}.toMutableList()
|
||||
|
||||
showFormatImportanceDialog(getString(R.string.format_importance_video), itms) {new ->
|
||||
editor.putString("format_importance_video", new.joinToString(",") { it.first }).apply()
|
||||
formatImportanceVideo.summary = new.map { it.second }.mapIndexed { index, s -> "${index + 1}. $s" }.joinToString("\n")
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun showFormatImportanceDialog(t: String, items: MutableList<Pair<String, String>>, onChange: (items: List<Pair<String, String>>) -> Unit){
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
builder.setTitle(t)
|
||||
val adapter = SortableTextItemAdapter(items)
|
||||
val itemTouchCallback = object : ItemTouchHelper.Callback() {
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
): Int {
|
||||
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
||||
return makeMovementFlags(dragFlags, 0)
|
||||
}
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
val itemToMove = adapter.items[viewHolder.absoluteAdapterPosition]
|
||||
adapter.items.remove(itemToMove)
|
||||
adapter.items.add(target.absoluteAdapterPosition, itemToMove)
|
||||
|
||||
adapter.notifyItemMoved(
|
||||
viewHolder.absoluteAdapterPosition,
|
||||
target.absoluteAdapterPosition
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
val linear = LinearLayout(requireActivity())
|
||||
linear.orientation = LinearLayout.VERTICAL
|
||||
|
||||
val note = TextView(requireActivity())
|
||||
note.text = getString(R.string.format_importance_note)
|
||||
note.textSize = 16f
|
||||
note.setTypeface(note.typeface, Typeface.BOLD)
|
||||
note.setPadding(20,20,20,20)
|
||||
linear.addView(note)
|
||||
|
||||
val recycler = RecyclerView(requireContext())
|
||||
recycler.layoutManager = LinearLayoutManager(requireContext())
|
||||
recycler.adapter = adapter
|
||||
|
||||
linear.addView(recycler)
|
||||
|
||||
val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
|
||||
itemTouchHelper.attachToRecyclerView(recycler)
|
||||
|
||||
|
||||
builder.setView(linear)
|
||||
builder.setPositiveButton(
|
||||
getString(android.R.string.ok)
|
||||
) { _: DialogInterface?, _: Int ->
|
||||
onChange(adapter.items)
|
||||
}
|
||||
|
||||
// handle the negative button of the alert dialog
|
||||
builder.setNegativeButton(
|
||||
getString(R.string.cancel)
|
||||
) { _: DialogInterface?, _: Int -> }
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.deniscerri.ytdl.util
|
||||
|
||||
import android.R.color
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
|
|
@ -37,9 +36,11 @@ import androidx.lifecycle.withStarted
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.deniscerri.ytdl.App
|
||||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.database.models.DownloadItem
|
||||
import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem
|
||||
import com.deniscerri.ytdl.database.repository.DownloadRepository
|
||||
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory
|
||||
import com.deniscerri.ytdl.util.Extensions.toTimePeriodsArray
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
|
|
@ -56,6 +57,7 @@ import java.util.Calendar
|
|||
import java.util.Locale
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
object Extensions {
|
||||
|
|
@ -289,19 +291,66 @@ object Extensions {
|
|||
}
|
||||
}
|
||||
|
||||
fun String.convertToTimestamp() : Int {
|
||||
enum class Period {
|
||||
HOUR, MINUTE, SECOND, MILLISECOND
|
||||
}
|
||||
|
||||
fun Long.toTimePeriodsArray() : Map<Period, Int> {
|
||||
var tmp = this
|
||||
val millis = ((tmp % 1000) / 100).toInt()
|
||||
tmp /= 1000
|
||||
val hours = (tmp / 3600).toInt()
|
||||
tmp %= 3600
|
||||
val minutes = (tmp / 60).toInt()
|
||||
tmp %= 60
|
||||
val seconds = tmp.toInt()
|
||||
|
||||
return mapOf(
|
||||
Period.HOUR to hours,
|
||||
Period.MINUTE to minutes,
|
||||
Period.SECOND to seconds,
|
||||
Period.MILLISECOND to millis
|
||||
)
|
||||
}
|
||||
|
||||
fun Long.toStringTimeStamp() : String {
|
||||
var tmp = this
|
||||
val millis = ((tmp % 1000) / 100).toInt()
|
||||
tmp /= 1000
|
||||
val hours = (tmp / 3600).toInt()
|
||||
tmp %= 3600
|
||||
val minutes = (tmp / 60).toInt()
|
||||
tmp %= 60
|
||||
val seconds = tmp.toInt()
|
||||
|
||||
var res = "${minutes.toString().padStart(if (hours > 0) 2 else 1, '0')}:${seconds.toString().padStart(2, '0')}"
|
||||
if (hours > 0){
|
||||
res = "${hours}:" + res
|
||||
}
|
||||
if (millis > 0){
|
||||
res += ".${millis}"
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
fun String.convertToTimestamp() : Long {
|
||||
return try {
|
||||
val timeArray = this.split(":")
|
||||
var timeSeconds = timeArray[timeArray.lastIndex].toInt()
|
||||
val secondsMillis = timeArray[timeArray.lastIndex]
|
||||
var timeSeconds = secondsMillis.split(".")[0].toLong()
|
||||
val millis = kotlin.runCatching { secondsMillis.split(".")[1].toInt() }.getOrElse { 0 }
|
||||
|
||||
var times = 60
|
||||
for (i in timeArray.lastIndex - 1 downTo 0) {
|
||||
timeSeconds += timeArray[i].toInt() * times
|
||||
times *= 60
|
||||
}
|
||||
timeSeconds
|
||||
|
||||
(timeSeconds * 1000) + millis * 100
|
||||
}catch (e: Exception){
|
||||
e.printStackTrace()
|
||||
0
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -408,11 +457,15 @@ object Extensions {
|
|||
paint.setColorFilter(PorterDuffColorFilter(colorValue.data, PorterDuff.Mode.SRC_IN))
|
||||
|
||||
val canvas = Canvas(bitmap)
|
||||
DrawableCompat.setTint(drawable, colorValue.data)
|
||||
DrawableCompat.setTint(drawable, ContextCompat.getColor(context, R.color.icon_fg))
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun DownloadItem.setAsScheduling(timeInMillis: Long) {
|
||||
status = DownloadRepository.Status.Scheduled.toString()
|
||||
downloadStartTime = timeInMillis
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ import android.util.Log
|
|||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.impl.utils.PreferenceUtils
|
||||
import com.anggrayudi.storage.callback.FileCallback
|
||||
import com.anggrayudi.storage.callback.FolderCallback
|
||||
import com.anggrayudi.storage.file.copyFolderTo
|
||||
|
|
@ -294,7 +296,8 @@ object FileUtil {
|
|||
}
|
||||
|
||||
fun getDownloadArchivePath(context: Context) : String {
|
||||
return context.filesDir.absolutePath + "/download_archive.txt"
|
||||
val folder = PreferenceManager.getDefaultSharedPreferences(context).getString("download_archive_path", context.filesDir.absolutePath + "/")!!
|
||||
return "${formatPath(folder)}download_archive.txt"
|
||||
}
|
||||
|
||||
fun getDefaultTerminalPath() : String {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
|
|||
import com.deniscerri.ytdl.util.Extensions.getIntByAny
|
||||
import com.deniscerri.ytdl.util.Extensions.getStringByAny
|
||||
import com.deniscerri.ytdl.util.Extensions.toStringDuration
|
||||
import com.deniscerri.ytdl.work.TerminalDownloadWorker
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
|
|
@ -488,6 +489,7 @@ class InfoUtil(private val context: Context) {
|
|||
if (proxy!!.isNotBlank()) {
|
||||
request.addOption("--proxy", proxy)
|
||||
}
|
||||
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
|
||||
|
||||
|
||||
|
||||
|
|
@ -546,7 +548,7 @@ class InfoUtil(private val context: Context) {
|
|||
if (proxy!!.isNotBlank()){
|
||||
request.addOption("--proxy", proxy)
|
||||
}
|
||||
|
||||
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
|
||||
|
||||
|
||||
YoutubeDL.getInstance().execute(request){ progress, _, line ->
|
||||
|
|
@ -631,6 +633,7 @@ class InfoUtil(private val context: Context) {
|
|||
if (proxy!!.isNotBlank()){
|
||||
request.addOption("--proxy", proxy)
|
||||
}
|
||||
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
|
||||
|
||||
|
||||
|
||||
|
|
@ -904,6 +907,7 @@ class InfoUtil(private val context: Context) {
|
|||
if (proxy!!.isNotBlank()){
|
||||
request.addOption("--proxy", proxy)
|
||||
}
|
||||
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
|
||||
|
||||
|
||||
|
||||
|
|
@ -1055,12 +1059,12 @@ class InfoUtil(private val context: Context) {
|
|||
}
|
||||
|
||||
if(downloadItem.title.isNotBlank()){
|
||||
request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.replace("\"", "\\\"").take(150)))
|
||||
request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.take(150)))
|
||||
}
|
||||
|
||||
|
||||
if (downloadItem.author.isNotBlank()){
|
||||
request.addCommands(listOf("--replace-in-metadata", "video:uploader", ".+", downloadItem.author.replace("\"", "\\\"").take(30)))
|
||||
request.addCommands(listOf("--replace-in-metadata", "video:uploader", ".+", downloadItem.author.take(30)))
|
||||
}
|
||||
|
||||
request.addOption("--parse-metadata", "uploader:(?P<uploader>.+)(?: - Topic)$")
|
||||
|
|
@ -1230,6 +1234,7 @@ class InfoUtil(private val context: Context) {
|
|||
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
|
||||
if (embedThumb) {
|
||||
request.addOption("--embed-thumbnail")
|
||||
if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1402,7 +1407,7 @@ class InfoUtil(private val context: Context) {
|
|||
|
||||
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
|
||||
request.addOption("--split-chapters")
|
||||
request.addOption("-o", "chapter:%(section_title)s.%(ext)s")
|
||||
request.addOption("-o", "chapter:%(section_number)d - %(section_title)s.%(ext)s")
|
||||
}else{
|
||||
if (filenameTemplate.isNotBlank()){
|
||||
request.addOption("-o", "${filenameTemplate.removeSuffix(".%(ext)s")}.%(ext)s")
|
||||
|
|
@ -1494,7 +1499,6 @@ class InfoUtil(private val context: Context) {
|
|||
}
|
||||
}
|
||||
audioFormats.forEachIndexed { idx, it -> formats.add(Format(audioFormatsValues[idx], containerPreference!!,"",acodecPreference!!, "",0, it)) }
|
||||
audioFormats.reverse()
|
||||
audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",resources.getString(R.string.preferred_format_id), "",1, it)) }
|
||||
return formats
|
||||
}
|
||||
|
|
@ -1520,7 +1524,7 @@ class InfoUtil(private val context: Context) {
|
|||
videoCodecs[videoCodecsValues.indexOf(this)]
|
||||
}
|
||||
}
|
||||
videoFormats.reversed().forEach { formats.add(Format(it, containerPreference!!,videoCodecPreference,audioCodecPreference, "",0, it.split("_")[0])) }
|
||||
videoFormats.forEach { formats.add(Format(it, containerPreference!!,videoCodecPreference,audioCodecPreference, "",0, it.split("_")[0])) }
|
||||
formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.preferred_format_id),"", "",1, it)) }
|
||||
return formats
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import android.os.Build
|
|||
import android.os.Bundle
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.navigation.NavDeepLinkBuilder
|
||||
import com.deniscerri.ytdl.R
|
||||
|
|
@ -33,6 +34,7 @@ class NotificationUtil(var context: Context) {
|
|||
private val commandDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID)
|
||||
private val finishedDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_FINISHED_CHANNEL_ID)
|
||||
private val erroredDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_ERRORED_CHANNEL_ID)
|
||||
private val miscDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_MISC_CHANNEL_ID)
|
||||
|
||||
private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java)
|
||||
private val resources: Resources = context.resources
|
||||
|
|
@ -99,6 +101,7 @@ class NotificationUtil(var context: Context) {
|
|||
DOWNLOAD_FINISHED_CHANNEL_ID -> { return finishedDownloadNotificationBuilder }
|
||||
DOWNLOAD_WORKER_CHANNEL_ID -> { return workerNotificationBuilder }
|
||||
DOWNLOAD_ERRORED_CHANNEL_ID -> { return erroredDownloadNotificationBuilder }
|
||||
DOWNLOAD_MISC_CHANNEL_ID -> { return miscDownloadNotificationBuilder }
|
||||
}
|
||||
return downloadNotificationBuilder
|
||||
}
|
||||
|
|
@ -336,13 +339,21 @@ class NotificationUtil(var context: Context) {
|
|||
.setArguments(bundle)
|
||||
.createPendingIntent()
|
||||
|
||||
val tabBundle = Bundle()
|
||||
tabBundle.putString("tab", "error")
|
||||
|
||||
val errorTabPendingIntent = NavDeepLinkBuilder(context)
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.downloadQueueMainFragment)
|
||||
.setArguments(tabBundle)
|
||||
.setArguments(bundleOf(Pair("tab", "error")))
|
||||
.createPendingIntent()
|
||||
|
||||
val reconfigurePendingItent = NavDeepLinkBuilder(context)
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.downloadQueueMainFragment)
|
||||
.setArguments(
|
||||
bundleOf(
|
||||
Pair("tab", "error"),
|
||||
Pair("reconfigure", id)
|
||||
)
|
||||
)
|
||||
.createPendingIntent()
|
||||
|
||||
notificationBuilder
|
||||
|
|
@ -359,6 +370,8 @@ class NotificationUtil(var context: Context) {
|
|||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.clearActions()
|
||||
|
||||
notificationBuilder.addAction(0, res.getString(R.string.configure_download), reconfigurePendingItent)
|
||||
if (logID != null){
|
||||
notificationBuilder.addAction(0, res.getString(R.string.logs), errorPendingIntent)
|
||||
}
|
||||
|
|
@ -618,6 +631,25 @@ class NotificationUtil(var context: Context) {
|
|||
notificationManager.notify(QUERY_PROCESS_FINISHED_NOTIFICATION_ID, notificationBuilder.build())
|
||||
}
|
||||
|
||||
fun createProcessingDownloads() : Notification{
|
||||
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
|
||||
|
||||
notificationBuilder
|
||||
.setContentTitle(resources.getString(R.string.processing))
|
||||
.setSmallIcon(R.drawable.ic_app_icon)
|
||||
.setLargeIcon(
|
||||
BitmapFactory.decodeResource(
|
||||
resources,
|
||||
R.drawable.ic_app_icon
|
||||
)
|
||||
)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.clearActions()
|
||||
|
||||
return notificationBuilder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DOWNLOAD_SERVICE_CHANNEL_ID = "1"
|
||||
const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2"
|
||||
|
|
|
|||
|
|
@ -617,7 +617,7 @@ object UiUtil {
|
|||
val command = bottomSheet.findViewById<Chip>(R.id.command)
|
||||
|
||||
when(status){
|
||||
DownloadRepository.Status.Queued, DownloadRepository.Status.QueuedPaused -> {
|
||||
DownloadRepository.Status.Scheduled -> {
|
||||
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) time!!.visibility = View.GONE
|
||||
else {
|
||||
val calendar = Calendar.getInstance()
|
||||
|
|
@ -712,7 +712,8 @@ object UiUtil {
|
|||
download!!.visibility = View.GONE
|
||||
}
|
||||
DownloadRepository.Status.Scheduled -> {
|
||||
download!!.text = context.getString(R.string.download_now)
|
||||
download!!.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_downloads, 0, 0, 0);
|
||||
download.text = context.getString(R.string.download_now)
|
||||
}
|
||||
else -> {
|
||||
download?.setOnLongClickListener {
|
||||
|
|
@ -813,6 +814,7 @@ object UiUtil {
|
|||
val codec = bottomSheet.findViewById<TextView>(R.id.codec)
|
||||
val fileSize = bottomSheet.findViewById<TextView>(R.id.file_size)
|
||||
val command = bottomSheet.findViewById<Chip>(R.id.command)
|
||||
val location = bottomSheet.findViewById<Chip>(R.id.location)
|
||||
val file = File(item.downloadPath.first())
|
||||
|
||||
val calendar = Calendar.getInstance()
|
||||
|
|
@ -858,6 +860,11 @@ object UiUtil {
|
|||
showGeneratedCommand(context, item.command)
|
||||
}
|
||||
|
||||
location?.isVisible = true
|
||||
location?.setOnClickListener {
|
||||
showFullTextDialog(context, item.downloadPath.joinToString("\n"), context.getString(R.string.location))
|
||||
}
|
||||
|
||||
|
||||
val link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link)
|
||||
val url = item.url
|
||||
|
|
@ -1664,169 +1671,6 @@ object UiUtil {
|
|||
errDialog.show()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun handleExistingDownloadsResponse(context: Activity, lifecycleScope: CoroutineScope, supportFragmentManager: FragmentManager, it: DownloadViewModel.AlreadyExistsUIState, downloadViewModel: DownloadViewModel, historyViewModel: HistoryViewModel){
|
||||
if (it.downloadItems.isEmpty() && it.historyItems.isEmpty()) return
|
||||
|
||||
val title = context.getString(R.string.download_already_exists)
|
||||
val message = context.getString(R.string.download_already_exists_summary)
|
||||
|
||||
val sheet = BottomSheetDialog(context)
|
||||
sheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
sheet.setContentView(R.layout.download_multiple_bottom_sheet)
|
||||
sheet.findViewById<TextView>(R.id.bottom_sheet_title)?.apply {
|
||||
text = title
|
||||
textSize = 20f
|
||||
}
|
||||
sheet.findViewById<TextView>(R.id.bottom_sheet_subtitle)?.text = message
|
||||
sheet.findViewById<LinearLayout>(R.id.multiple_top_info)?.isVisible = false
|
||||
|
||||
var adapter: AlreadyExistsAdapter? = null
|
||||
val list = mutableListOf<Pair<DownloadItem, Long?>>()
|
||||
val alreadyExistsClickListener = object: AlreadyExistsAdapter.OnItemClickListener {
|
||||
|
||||
override fun onEditItem(downloadItem: DownloadItem, position: Int) {
|
||||
val resultItem = downloadViewModel.createResultItemFromDownload(downloadItem)
|
||||
val onItemUpdated = object: ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
|
||||
override fun onDownloadItemUpdate(
|
||||
resultItemID: Long,
|
||||
item: DownloadItem
|
||||
) {
|
||||
val currentIndex = list.indexOfFirst { it.first.id == item.id }
|
||||
val current = list[currentIndex]
|
||||
list[currentIndex] = Pair(item, current.second)
|
||||
adapter?.submitList(list)
|
||||
adapter?.notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem, onItemUpdated)
|
||||
bottomSheet.show(supportFragmentManager, "configureDownloadSingleSheet")
|
||||
}
|
||||
|
||||
override fun onDeleteItem(downloadItem: DownloadItem, position: Int, historyID: Long?) {
|
||||
showGenericDeleteDialog(context, downloadItem.title){
|
||||
if (historyID == null){
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
downloadViewModel.deleteDownload(downloadItem.id)
|
||||
}
|
||||
}
|
||||
list.removeAll { it.first.id == downloadItem.id }
|
||||
if (list.isEmpty()){
|
||||
sheet.dismiss()
|
||||
}
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShowHistoryItem(id: Long) {
|
||||
lifecycleScope.launch {
|
||||
val historyItem = withContext(Dispatchers.IO){
|
||||
downloadViewModel.getHistoryItemById(id)
|
||||
}
|
||||
showHistoryItemDetailsCard(historyItem, context, isPresent = true,
|
||||
removeItem = { item, deleteFile ->
|
||||
historyViewModel.delete(item, deleteFile)
|
||||
},
|
||||
redownloadItem = { },
|
||||
redownloadShowDownloadCard = {}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
adapter = AlreadyExistsAdapter(alreadyExistsClickListener, context)
|
||||
sheet.findViewById<RecyclerView>(R.id.downloadMultipleRecyclerview)?.apply {
|
||||
layoutManager = GridLayoutManager(context, 1)
|
||||
this.adapter = adapter
|
||||
enableFastScroll()
|
||||
setPadding(0,0,0,0)
|
||||
}
|
||||
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
it.downloadItems.forEach {
|
||||
val downloadItem = downloadViewModel.getItemByID(it)
|
||||
list.add(Pair(downloadItem, null))
|
||||
adapter.submitList(list)
|
||||
}
|
||||
|
||||
it.historyItems.forEach {
|
||||
val historyItem = downloadViewModel.getHistoryItemById(it) ?: return@forEach
|
||||
val downloadItem = downloadViewModel.createDownloadItemFromHistory(historyItem)
|
||||
downloadItem.id = it
|
||||
list.add(Pair(downloadItem, historyItem.id))
|
||||
adapter.submitList(list)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sheet.findViewById<Button>(R.id.bottomsheet_download_button)?.apply {
|
||||
setOnClickListener {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
downloadViewModel.deleteProcessing()
|
||||
val items = list.map { it.first }
|
||||
items.forEach { it.id = 0 }
|
||||
downloadViewModel.queueDownloads(items, true)
|
||||
withContext(Dispatchers.Main){
|
||||
sheet.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sheet.findViewById<Button>(R.id.bottomsheet_schedule_button)?.apply {
|
||||
setOnClickListener {
|
||||
showDatePicker(supportFragmentManager) { calendar ->
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
downloadViewModel.deleteProcessing()
|
||||
val items = list.map { it.first }
|
||||
items.forEach {
|
||||
it.id = 0
|
||||
it.downloadStartTime = calendar.timeInMillis
|
||||
}
|
||||
|
||||
|
||||
runBlocking {
|
||||
downloadViewModel.queueDownloads(items, true)
|
||||
val first = items.first()
|
||||
val date = SimpleDateFormat(
|
||||
DateFormat.getBestDateTimePattern(
|
||||
Locale.getDefault(),
|
||||
"ddMMMyyyy - HHmm"
|
||||
), Locale.getDefault()
|
||||
).format(first.downloadStartTime)
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.download_rescheduled_to) + " " + date,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
sheet.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sheet.setOnDismissListener { _ ->
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
downloadViewModel.deleteProcessing()
|
||||
downloadViewModel.deleteAllWithID(it.downloadItems)
|
||||
}
|
||||
}
|
||||
|
||||
sheet.findViewById<BottomAppBar>(R.id.bottomAppBar)?.isVisible = false
|
||||
sheet.show()
|
||||
}
|
||||
|
||||
|
||||
fun showGenericDeleteDialog(context: Context, itemTitle: String, accepted: () -> Unit){
|
||||
val deleteDialog = MaterialAlertDialogBuilder(context)
|
||||
deleteDialog.setTitle(context.getString(R.string.you_are_going_to_delete) + " \"" + itemTitle + "\"!")
|
||||
|
|
|
|||
|
|
@ -1,18 +1,28 @@
|
|||
package com.deniscerri.ytdl.work
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import com.deniscerri.ytdl.receiver.CancelScheduleAlarmReceiver
|
||||
import com.deniscerri.ytdl.receiver.ScheduleAlarmReceiver
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class AlarmScheduler(private val context: Context) {
|
||||
|
||||
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
|
||||
|
||||
@SuppressLint("ScheduleExactAlarm")
|
||||
fun schedule() {
|
||||
cancel()
|
||||
|
||||
|
|
@ -25,17 +35,15 @@ class AlarmScheduler(private val context: Context) {
|
|||
val time = calculateNextTime(sTime)
|
||||
|
||||
//schedule starting work
|
||||
val workConstraints = Constraints.Builder()
|
||||
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
|
||||
.addTag("scheduledDownload")
|
||||
.addTag("download")
|
||||
.setConstraints(workConstraints.build())
|
||||
.setInitialDelay(System.currentTimeMillis() - time.timeInMillis, TimeUnit.MILLISECONDS)
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||
System.currentTimeMillis().toString(),
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
workRequest.build()
|
||||
alarmManager?.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
time.timeInMillis,
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
Intent(context, ScheduleAlarmReceiver::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
|
||||
//schedule closing work
|
||||
|
|
@ -46,27 +54,35 @@ class AlarmScheduler(private val context: Context) {
|
|||
sTime.set(Calendar.SECOND, 0)
|
||||
val calendar = calculateNextTime(eTime)
|
||||
|
||||
|
||||
val workRequest2 = OneTimeWorkRequestBuilder<CancelScheduledDownloadWorker>()
|
||||
.addTag("cancelScheduledDownload")
|
||||
.setConstraints(workConstraints.build())
|
||||
.setInitialDelay(System.currentTimeMillis() - calendar.timeInMillis + 60000, TimeUnit.MILLISECONDS)
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||
System.currentTimeMillis().toString(),
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
workRequest2.build()
|
||||
alarmManager?.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.timeInMillis,
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
Intent(context, CancelScheduleAlarmReceiver::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
val wm = WorkManager.getInstance(context)
|
||||
wm.cancelAllWorkByTag("scheduledDownload")
|
||||
wm.cancelAllWorkByTag("cancelScheduledDownload")
|
||||
val intent = Intent(context, ScheduleAlarmReceiver::class.java)
|
||||
val pendingIntent = PendingIntent.getService(context, 0, intent,
|
||||
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
|
||||
val cancelIntent = Intent(context, CancelScheduleAlarmReceiver::class.java)
|
||||
val cancelPendingIntent = PendingIntent.getService(context, 0, cancelIntent,
|
||||
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
kotlin.runCatching {
|
||||
alarmManager?.cancel(pendingIntent)
|
||||
alarmManager?.cancel(cancelPendingIntent)
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateNextTime(c: Calendar) : Calendar {
|
||||
private fun calculateNextTime(c: Calendar) : Calendar {
|
||||
val calendar = Calendar.getInstance()
|
||||
if (c.get(Calendar.HOUR_OF_DAY) < calendar.get(Calendar.HOUR_OF_DAY)){
|
||||
c.add(Calendar.DATE, 1)
|
||||
|
|
@ -110,4 +126,12 @@ class AlarmScheduler(private val context: Context) {
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
fun canSchedule() : Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= 31){
|
||||
alarmManager?.canScheduleExactAlarms() == true
|
||||
}else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,15 +58,9 @@ class CancelScheduledDownloadWorker(
|
|||
WorkManager.getInstance(context).cancelAllWorkByTag("download")
|
||||
runningDownloads.forEach {
|
||||
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
|
||||
it.status = DownloadRepository.Status.Queued.toString()
|
||||
dao.update(it)
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
|
||||
|
||||
companion object {
|
||||
val runningYTDLInstances: MutableList<Long> = mutableListOf()
|
||||
const val TAG = "DownloadWorker"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -119,7 +119,10 @@ class DownloadWorker(
|
|||
delay(1500)
|
||||
//update item if its incomplete
|
||||
resultRepo.updateDownloadItem(downloadItem)?.apply {
|
||||
dao.updateWithoutUpsert(this)
|
||||
val status = dao.checkStatus(this.id)
|
||||
if (listOf(DownloadRepository.Status.Active, DownloadRepository.Status.ActivePaused).contains(status)){
|
||||
dao.updateWithoutUpsert(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,9 +175,7 @@ class DownloadWorker(
|
|||
}
|
||||
}
|
||||
}.onSuccess {
|
||||
resultRepo.updateDownloadItem(downloadItem)?.apply {
|
||||
dao.updateWithoutUpsert(this)
|
||||
}
|
||||
resultRepo.updateDownloadItem(downloadItem)
|
||||
val wasQuickDownloaded = resultDao.getCountInt() == 0
|
||||
runBlocking {
|
||||
var finalPaths : MutableList<String>?
|
||||
|
|
@ -241,9 +242,9 @@ class DownloadWorker(
|
|||
val incognito = sharedPreferences.getBoolean("incognito", false)
|
||||
if (!incognito) {
|
||||
if (request.hasOption("--download-archive") && finalPaths == listOf(context.getString(R.string.unfound_file))) {
|
||||
Looper.prepare().run {
|
||||
handler.postDelayed({
|
||||
Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, 100)
|
||||
}else{
|
||||
val unixTime = System.currentTimeMillis() / 1000
|
||||
finalPaths?.apply {
|
||||
|
|
@ -303,6 +304,8 @@ class DownloadWorker(
|
|||
}
|
||||
|
||||
}.onFailure {
|
||||
if (this@DownloadWorker.isStopped) return@onFailure
|
||||
|
||||
FileUtil.deleteConfigFiles(request)
|
||||
|
||||
withContext(Dispatchers.Main){
|
||||
|
|
@ -345,7 +348,6 @@ class DownloadWorker(
|
|||
|
||||
setProgressAsync(workDataOf("progress" to 100, "output" to it.toString(), "id" to downloadItem.id))
|
||||
}
|
||||
//if (items.size <= 1) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.work.CoroutineWorker
|
|||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
|
|
@ -205,7 +206,14 @@ class ObserveSourceWorker(
|
|||
}
|
||||
|
||||
//schedule for next time
|
||||
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
|
||||
|
||||
val workConstraints = Constraints.Builder()
|
||||
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
else {
|
||||
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
}
|
||||
|
||||
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
|
||||
.addTag("observeSources")
|
||||
.addTag(sourceID.toString())
|
||||
|
|
|
|||
|
|
@ -223,55 +223,49 @@
|
|||
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/home_fabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="bottom|end">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/copied_url_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="10dp"
|
||||
android:layout_gravity="bottom|end">
|
||||
android:layout_gravity="bottom|end"
|
||||
android:text="@string/link_you_copied"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:contentDescription="@string/link_you_copied"
|
||||
app:icon="@drawable/ic_clipboard"
|
||||
app:useCompatPadding="true" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/copied_url_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:text="@string/link_you_copied"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:contentDescription="@string/link_you_copied"
|
||||
app:icon="@drawable/ic_clipboard"
|
||||
app:useCompatPadding="true" />
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_all_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:text="@string/download_all"
|
||||
app:icon="@drawable/ic_down" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_all_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/download_all"
|
||||
app:icon="@drawable/ic_down" />
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_selected_fab"
|
||||
android:visibility="gone"
|
||||
app:elevation="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
app:icon="@drawable/ic_down"
|
||||
android:text="@string/download"
|
||||
app:srcCompat="@drawable/ic_music"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_selected_fab"
|
||||
android:visibility="gone"
|
||||
app:elevation="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
app:icon="@drawable/ic_down"
|
||||
android:text="@string/download"
|
||||
app:srcCompat="@drawable/ic_music"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
@ -234,55 +234,49 @@
|
|||
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/home_fabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="bottom|end">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/copied_url_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="10dp"
|
||||
android:layout_gravity="bottom|end">
|
||||
android:layout_gravity="bottom|end"
|
||||
android:text="@string/link_you_copied"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:contentDescription="@string/link_you_copied"
|
||||
app:icon="@drawable/ic_clipboard"
|
||||
app:useCompatPadding="true" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/copied_url_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:text="@string/link_you_copied"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:contentDescription="@string/link_you_copied"
|
||||
app:icon="@drawable/ic_clipboard"
|
||||
app:useCompatPadding="true" />
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_all_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:text="@string/download_all"
|
||||
app:icon="@drawable/ic_down" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_all_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/download_all"
|
||||
app:icon="@drawable/ic_down" />
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_selected_fab"
|
||||
android:visibility="gone"
|
||||
app:elevation="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
app:icon="@drawable/ic_down"
|
||||
android:text="@string/download"
|
||||
app:srcCompat="@drawable/ic_music"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_selected_fab"
|
||||
android:visibility="gone"
|
||||
app:elevation="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
app:icon="@drawable/ic_down"
|
||||
android:text="@string/download"
|
||||
app:srcCompat="@drawable/ic_music"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
@ -223,55 +223,49 @@
|
|||
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/home_fabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="bottom|end">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/copied_url_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="10dp"
|
||||
android:layout_gravity="bottom|end">
|
||||
android:layout_gravity="bottom|end"
|
||||
android:text="@string/link_you_copied"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:contentDescription="@string/link_you_copied"
|
||||
app:icon="@drawable/ic_clipboard"
|
||||
app:useCompatPadding="true" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/copied_url_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:text="@string/link_you_copied"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:contentDescription="@string/link_you_copied"
|
||||
app:icon="@drawable/ic_clipboard"
|
||||
app:useCompatPadding="true" />
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_all_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:text="@string/download_all"
|
||||
app:icon="@drawable/ic_down" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_all_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/download_all"
|
||||
app:icon="@drawable/ic_down" />
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_selected_fab"
|
||||
android:visibility="gone"
|
||||
app:elevation="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
app:icon="@drawable/ic_down"
|
||||
android:text="@string/download"
|
||||
app:srcCompat="@drawable/ic_music"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_selected_fab"
|
||||
android:visibility="gone"
|
||||
app:elevation="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
app:icon="@drawable/ic_down"
|
||||
android:text="@string/download"
|
||||
app:srcCompat="@drawable/ic_music"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
@ -253,56 +253,50 @@
|
|||
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/home_fabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="bottom|end">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/copied_url_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="10dp"
|
||||
android:layout_gravity="bottom|end">
|
||||
android:layout_gravity="bottom|end"
|
||||
android:text="@string/link_you_copied"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:contentDescription="@string/link_you_copied"
|
||||
app:icon="@drawable/ic_clipboard"
|
||||
app:useCompatPadding="true" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/copied_url_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:text="@string/link_you_copied"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:contentDescription="@string/link_you_copied"
|
||||
app:icon="@drawable/ic_clipboard"
|
||||
app:useCompatPadding="true" />
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_all_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:text="@string/download_all"
|
||||
app:icon="@drawable/ic_down" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_all_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/download_all"
|
||||
app:icon="@drawable/ic_down" />
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_selected_fab"
|
||||
android:visibility="gone"
|
||||
app:elevation="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
app:icon="@drawable/ic_down"
|
||||
android:text="@string/download"
|
||||
app:srcCompat="@drawable/ic_music"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_selected_fab"
|
||||
android:visibility="gone"
|
||||
app:elevation="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
app:icon="@drawable/ic_down"
|
||||
android:text="@string/download"
|
||||
app:srcCompat="@drawable/ic_music"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
129
app/src/main/res/layout/adjust_cut_timestamp.xml
Normal file
129
app/src/main/res/layout/adjust_cut_timestamp.xml
Normal 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>
|
||||
|
|
@ -76,14 +76,12 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
android:maxLines="2"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:scrollbars="none"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@+id/horizontalScrollView"
|
||||
app:layout_constraintEnd_toStartOf="@+id/already_exists_buttons"
|
||||
app:layout_constraintEnd_toStartOf="@+id/options"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
|
@ -92,10 +90,9 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="title,already_exists_buttons" />
|
||||
app:constraint_referenced_ids="title,options" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/horizontalScrollView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="bottom"
|
||||
|
|
@ -112,6 +109,44 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_type"
|
||||
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@drawable/rounded_corner"
|
||||
android:backgroundTint="?attr/colorSecondary"
|
||||
android:clickable="false"
|
||||
android:gravity="center"
|
||||
android:minWidth="30dp"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
app:cornerRadius="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/file_size"
|
||||
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@drawable/rounded_corner"
|
||||
android:backgroundTint="?attr/colorSecondary"
|
||||
android:clickable="false"
|
||||
android:gravity="center"
|
||||
android:minWidth="30dp"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
app:cornerRadius="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/format_note"
|
||||
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
|
||||
|
|
@ -153,24 +188,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/file_size"
|
||||
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@drawable/rounded_corner"
|
||||
android:backgroundTint="?attr/colorSecondary"
|
||||
android:clickable="false"
|
||||
android:gravity="center"
|
||||
android:minWidth="30dp"
|
||||
android:paddingHorizontal="5dp"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
app:cornerRadius="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -178,39 +196,25 @@
|
|||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/already_exists_buttons"
|
||||
android:layout_width="wrap_content"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/options"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="0dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="0dp"
|
||||
app:cornerRadius="10dp"
|
||||
app:icon="@drawable/baseline_more_vert_24"
|
||||
app:iconTint="?attr/colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0">
|
||||
|
||||
<Button
|
||||
android:id="@+id/already_exists_edit"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_edit"
|
||||
android:contentDescription="@string/edit"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintVertical_bias="1.0" />
|
||||
|
||||
|
||||
<Button
|
||||
android:id="@+id/already_exists_delete"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled"
|
||||
android:layout_width="wrap_content"
|
||||
android:contentDescription="@string/Remove"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/baseline_delete_24"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</LinearLayout>
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
|
@ -95,6 +95,23 @@
|
|||
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
|
||||
app:layout_constraintEnd_toEndOf="@+id/frame_layout" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/forward"
|
||||
android:clickable="false"
|
||||
app:iconSize="50dp"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
app:iconTint="?android:colorAccent"
|
||||
app:icon="@drawable/exomedia_ic_fast_forward_white"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:contentDescription="@string/end"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/frame_layout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/durationText"
|
||||
|
|
@ -162,6 +179,7 @@
|
|||
android:hint="@string/start">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/from_textinput_edittext"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
|
|
@ -189,6 +207,7 @@
|
|||
app:layout_constraintStart_toEndOf="@+id/colon">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/to_textinput_edittext"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
|
|
|
|||
|
|
@ -124,12 +124,18 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loadingItemsProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:indeterminate="true"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/downloadMultipleRecyclerview"
|
||||
android:layout_width="match_parent"
|
||||
android:paddingTop="10dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="70dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
|
||||
|
|
|
|||
|
|
@ -17,91 +17,64 @@
|
|||
android:id="@+id/scr"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="10dp"
|
||||
android:scrollbars="none"
|
||||
android:paddingTop="20dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/current"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginVertical="5dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="20dp">
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/use_extra_commands"
|
||||
android:text="@string/current"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:textIsSelectable="true"
|
||||
android:id="@+id/currentText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/current"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginVertical="5dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/current"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:textIsSelectable="true"
|
||||
android:id="@+id/currentText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:contentDescription="@string/extra_command"
|
||||
android:hint="@string/extra_command"
|
||||
app:layout_constraintTop_toBottomOf="@+id/scr">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/command"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="14sp"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="2000" />
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
android:id="@+id/file_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
|
@ -37,7 +39,7 @@
|
|||
android:scrollbars="none"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
|
|
@ -114,19 +116,6 @@
|
|||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/action_button"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="0dp"
|
||||
android:padding="0dp"
|
||||
app:cornerRadius="10dp"
|
||||
app:iconTint="?attr/colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
android:scrollbars="none"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:paddingBottom="100dp"
|
||||
android:layout_marginBottom="100dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/pause_resume">
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
|
|
|
|||
74
app/src/main/res/layout/fragment_already_exists_dialog.xml
Normal file
74
app/src/main/res/layout/fragment_already_exists_dialog.xml
Normal 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>
|
||||
|
|
@ -67,6 +67,7 @@
|
|||
android:id="@+id/container_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textDirection="locale"
|
||||
android:inputType="none"
|
||||
app:simpleItems="@array/audio_containers"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -152,17 +152,12 @@
|
|||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/home_fabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="10dp"
|
||||
android:layout_gravity="bottom|end">
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="bottom|end">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/copied_url_fab"
|
||||
|
|
@ -176,31 +171,30 @@
|
|||
app:icon="@drawable/ic_clipboard"
|
||||
app:useCompatPadding="true" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_all_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:text="@string/download_all"
|
||||
app:icon="@drawable/ic_down" />
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_all_fab"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:text="@string/download_all"
|
||||
app:icon="@drawable/ic_down" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_selected_fab"
|
||||
android:visibility="gone"
|
||||
app:elevation="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
app:icon="@drawable/ic_down"
|
||||
android:text="@string/download"
|
||||
app:srcCompat="@drawable/ic_music"/>
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/download_selected_fab"
|
||||
android:visibility="gone"
|
||||
app:elevation="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
app:icon="@drawable/ic_down"
|
||||
android:text="@string/download"
|
||||
app:srcCompat="@drawable/ic_music"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
@ -155,6 +155,21 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/location"
|
||||
android:visibility="gone"
|
||||
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="false"
|
||||
android:minWidth="30dp"
|
||||
app:cornerRadius="10dp"
|
||||
android:text="@string/location"
|
||||
app:chipIcon="@drawable/baseline_folder_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
|
|
|||
|
|
@ -93,6 +93,8 @@
|
|||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/download_item_data"
|
||||
android:layout_width="0dp"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
@ -232,6 +234,8 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="0dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="0dp"
|
||||
app:cornerRadius="10dp"
|
||||
app:icon="@drawable/baseline_more_vert_24"
|
||||
|
|
|
|||
|
|
@ -129,6 +129,13 @@
|
|||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loadingItemsProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:indeterminate="true"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/downloadMultipleRecyclerview"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
41
app/src/main/res/layout/sortable_text_item.xml
Normal file
41
app/src/main/res/layout/sortable_text_item.xml
Normal 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>
|
||||
|
||||
16
app/src/main/res/menu/already_exists_menu.xml
Normal file
16
app/src/main/res/menu/already_exists_menu.xml
Normal 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>
|
||||
42
app/src/main/res/menu/scheduled_downloads_menu_context.xml
Normal file
42
app/src/main/res/menu/scheduled_downloads_menu_context.xml
Normal 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>
|
||||
|
||||
|
|
@ -33,6 +33,9 @@
|
|||
<action
|
||||
android:id="@+id/action_homeFragment_to_downloadMultipleBottomSheetDialog2"
|
||||
app:destination="@id/downloadMultipleBottomSheetDialog2" />
|
||||
<action
|
||||
android:id="@+id/action_homeFragment_to_downloadsAlreadyExistDialog"
|
||||
app:destination="@id/downloadsAlreadyExistDialog" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/historyFragment"
|
||||
|
|
@ -42,6 +45,9 @@
|
|||
<action
|
||||
android:id="@+id/action_historyFragment_to_downloadQueueMainFragment"
|
||||
app:destination="@id/downloadQueueMainFragment" />
|
||||
<action
|
||||
android:id="@+id/action_historyFragment_to_downloadsAlreadyExistDialog"
|
||||
app:destination="@id/downloadsAlreadyExistDialog" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/moreFragment"
|
||||
|
|
@ -191,4 +197,8 @@
|
|||
android:id="@+id/observeSourcesBottomSheetDialog"
|
||||
android:name="com.deniscerri.ytdl.ui.downloadcard.ObserveSourcesBottomSheetDialog"
|
||||
android:label="ObserveSourcesBottomSheetDialog" />
|
||||
<dialog
|
||||
android:id="@+id/downloadsAlreadyExistDialog"
|
||||
android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadsAlreadyExistDialog"
|
||||
android:label="DownloadsAlreadyExistDialog" />
|
||||
</navigation>
|
||||
|
|
@ -34,4 +34,8 @@
|
|||
android:id="@+id/downloadMultipleBottomSheetDialog"
|
||||
android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadMultipleBottomSheetDialog"
|
||||
android:label="DownloadMultipleBottomSheetDialog" />
|
||||
<dialog
|
||||
android:id="@+id/downloadsAlreadyExistDialog2"
|
||||
android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadsAlreadyExistDialog"
|
||||
android:label="DownloadsAlreadyExistDialog" />
|
||||
</navigation>
|
||||
|
|
@ -40,37 +40,37 @@
|
|||
</string-array>
|
||||
|
||||
<string-array name="video_formats">
|
||||
<item>@string/worst_quality</item>
|
||||
<item>240p</item>
|
||||
<item>360p</item>
|
||||
<item>480p</item>
|
||||
<item>720p</item>
|
||||
<item>1080p</item>
|
||||
<item>1440p</item>
|
||||
<item>2160p</item>
|
||||
<item>@string/best_quality</item>
|
||||
<item>2160p</item>
|
||||
<item>1440p</item>
|
||||
<item>1080p</item>
|
||||
<item>720p</item>
|
||||
<item>480p</item>
|
||||
<item>360p</item>
|
||||
<item>240p</item>
|
||||
<item>@string/worst_quality</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="video_formats_values">
|
||||
<item>worst</item>
|
||||
<item>240p_ytdlnisgeneric</item>
|
||||
<item>360p_ytdlnisgeneric</item>
|
||||
<item>480p_ytdlnisgeneric</item>
|
||||
<item>720p_ytdlnisgeneric</item>
|
||||
<item>1080p_ytdlnisgeneric</item>
|
||||
<item>1440p_ytdlnisgeneric</item>
|
||||
<item>2160p_ytdlnisgeneric</item>
|
||||
<item>best</item>
|
||||
<item>2160p_ytdlnisgeneric</item>
|
||||
<item>1440p_ytdlnisgeneric</item>
|
||||
<item>1080p_ytdlnisgeneric</item>
|
||||
<item>720p_ytdlnisgeneric</item>
|
||||
<item>480p_ytdlnisgeneric</item>
|
||||
<item>360p_ytdlnisgeneric</item>
|
||||
<item>240p_ytdlnisgeneric</item>
|
||||
<item>worst</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="audio_formats">
|
||||
<item>worst</item>
|
||||
<item>best</item>
|
||||
<item>worst</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="audio_formats_values">
|
||||
<item>wa</item>
|
||||
<item>ba</item>
|
||||
<item>wa</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="format_ordering">
|
||||
|
|
@ -896,6 +896,7 @@
|
|||
<item>%(uploader)s___(string): Full name of the video uploader)</item>
|
||||
<item>%(title)s___(string): Video title)</item>
|
||||
<item>%(id)s___(string): Video identifier)</item>
|
||||
<item>%(playlist_index,playlist_autonumber&{}. |)s%(title)s___(string): Title with playlist index in front if its available</item>
|
||||
<item>%(fulltitle)s___(string): Video title ignoring live timestamp and generic title)</item>
|
||||
<!-- <item>%(ext)s___(string): Video filename extension)</item>-->
|
||||
<item>%(alt_title)s___(string): A secondary title of the video)</item>
|
||||
|
|
@ -1418,4 +1419,34 @@
|
|||
<item>config</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="format_importance_audio">
|
||||
<item>@string/preferred_format_id</item>
|
||||
<item>@string/language</item>
|
||||
<item>@string/codec</item>
|
||||
<item>@string/container</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="format_importance_audio_values">
|
||||
<item>id</item>
|
||||
<item>language</item>
|
||||
<item>codec</item>
|
||||
<item>container</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="format_importance_video">
|
||||
<item>@string/preferred_format_id</item>
|
||||
<item>@string/video_quality</item>
|
||||
<item>@string/codec</item>
|
||||
<item>@string/no_audio</item>
|
||||
<item>@string/container</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="format_importance_video_values">
|
||||
<item>id</item>
|
||||
<item>resolution</item>
|
||||
<item>codec</item>
|
||||
<item>no_audio</item>
|
||||
<item>container</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
|
@ -391,4 +391,15 @@
|
|||
<string name="move_bottom">Move to the Bottom</string>
|
||||
<string name="scheduled">Scheduled</string>
|
||||
<string name="clear_scheduled">Clear Scheduled</string>
|
||||
<string name="minute">Minute</string>
|
||||
<string name="second">Second</string>
|
||||
<string name="milliseconds">Milliseconds</string>
|
||||
<string name="format_importance_audio">Format Importance Order (Audio)</string>
|
||||
<string name="format_importance_video">Format Importance Order (Video)</string>
|
||||
<string name="format_importance_note">Adjust format element importance. This order will only be used when the app fetches the formats in the download card and auto-selects the format!</string>
|
||||
<string name="no_audio">Video with no Audio</string>
|
||||
<string name="download_archive_folder">Download Archive Folder</string>
|
||||
<string name="location">Location</string>
|
||||
<string name="enable_alarm_permission">You need to enable the SCHEDULE_EXACT_ALARM permission in the app settings.</string>
|
||||
<string name="process_downloads_background">Downloads are still loading. Continue procesing them in the background and start downloading?</string>
|
||||
</resources>
|
||||
|
|
@ -37,6 +37,12 @@
|
|||
app:useSimpleSummaryProvider="true"
|
||||
app:title="@string/prevent_duplicate_downloads" />
|
||||
|
||||
<Preference
|
||||
android:defaultValue=""
|
||||
app:icon="@drawable/baseline_archive_24"
|
||||
app:key="download_archive_path"
|
||||
app:title="@string/download_archive_folder" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:widgetLayout="@layout/preferece_material_switch"
|
||||
app:defaultValue="true"
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
<PreferenceCategory android:title="@string/misc">
|
||||
<SwitchPreferenceCompat
|
||||
android:widgetLayout="@layout/preferece_material_switch"
|
||||
app:defaultValue="false"
|
||||
app:defaultValue="true"
|
||||
android:icon="@drawable/baseline_recommend_24"
|
||||
android:key="home_recommendations"
|
||||
app:summary="@string/video_recommendations_summary"
|
||||
|
|
@ -145,7 +145,7 @@
|
|||
|
||||
<SwitchPreferenceCompat
|
||||
android:widgetLayout="@layout/preferece_material_switch"
|
||||
app:defaultValue="false"
|
||||
app:defaultValue="true"
|
||||
android:icon="@drawable/baseline_numbers_24"
|
||||
android:key="show_count_downloads"
|
||||
app:title="@string/show_download_count" />
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@
|
|||
android:summary="@string/use_extra_commands_summary"
|
||||
app:title="@string/use_extra_commands" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:widgetLayout="@layout/preferece_material_switch"
|
||||
app:defaultValue="false"
|
||||
app:icon="@drawable/ic_cut"
|
||||
app:key="force_keyframes"
|
||||
app:summary="@string/force_keyframes_summary"
|
||||
app:title="@string/force_keyframes" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/audio">
|
||||
|
|
@ -82,14 +90,6 @@
|
|||
app:summary="@string/crop_thumb_summary"
|
||||
app:title="@string/crop_thumb" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:widgetLayout="@layout/preferece_material_switch"
|
||||
app:defaultValue="false"
|
||||
app:icon="@drawable/ic_cut"
|
||||
app:key="force_keyframes"
|
||||
app:summary="@string/force_keyframes_summary"
|
||||
app:title="@string/force_keyframes" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
|
||||
|
|
@ -144,7 +144,7 @@
|
|||
app:title="@string/save_thumb" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="png"
|
||||
android:defaultValue="jpg"
|
||||
android:entries="@array/thumbnail_containers"
|
||||
android:entryValues="@array/thumbnail_containers_values"
|
||||
android:icon="@drawable/ic_image"
|
||||
|
|
@ -254,6 +254,17 @@
|
|||
app:icon="@drawable/ic_format"
|
||||
app:key="prefer_smaller_formats"
|
||||
app:title="@string/prefer_smaller_formats" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_format"
|
||||
android:key="format_importance_audio"
|
||||
app:title="@string/format_importance_audio" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_format"
|
||||
android:key="format_importance_video"
|
||||
app:title="@string/format_importance_video" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
|
||||
<SwitchPreferenceCompat
|
||||
android:widgetLayout="@layout/preferece_material_switch"
|
||||
app:defaultValue="false"
|
||||
app:defaultValue="true"
|
||||
android:icon="@drawable/ic_update_app"
|
||||
android:key="update_app"
|
||||
app:summary="@string/update_app_summary"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ buildscript {
|
|||
commonsIoVer = '2.5'
|
||||
// supports java 1.6
|
||||
commonsCompressVer = '1.12'
|
||||
youtubedlAndroidVer = "-SNAPSHOT"
|
||||
youtubedlAndroidVer = "ed652962e5"
|
||||
youtubedlAndroidVerNoPyCrypto = "97bee6c0e8"
|
||||
oldytdlAndroidVer = "0.14.0"
|
||||
workVer = "2.9.0"
|
||||
|
|
@ -37,12 +37,12 @@ buildscript {
|
|||
}
|
||||
|
||||
plugins {
|
||||
id 'com.android.application' version '8.3.1' apply false
|
||||
id 'com.android.library' version '8.3.1' apply false
|
||||
id 'com.android.application' version '8.4.0' apply false
|
||||
id 'com.android.library' version '8.4.0' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
|
||||
id "org.jetbrains.kotlin.plugin.serialization" version "1.8.10" apply false
|
||||
id "org.jetbrains.kotlin.plugin.parcelize" version "1.8.10" apply false
|
||||
id 'com.android.test' version '8.3.1' apply false
|
||||
id 'com.android.test' version '8.4.0' apply false
|
||||
id 'com.google.devtools.ksp' version '1.8.10-1.0.9' apply false
|
||||
}
|
||||
|
||||
|
|
|
|||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
|||
#Wed Oct 18 18:36:39 CEST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
Loading…
Reference in a new issue