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