1.7.9.3 pre

This commit is contained in:
zaednasr 2024-10-17 22:41:07 +02:00
parent 3307118d7f
commit 56b503a46b
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
49 changed files with 1016 additions and 553 deletions

View file

@ -1,11 +1,13 @@
# YTDLnis Changelog # YTDLnis Changelog
> # 1.7.9.2 > # 1.7.9.2 (2024-09)
# What's Changed # What's Changed
- newpipe extractor general fixes - newpipe extractor general fixes
- fixed newpipe formats not working
- removed piped because its more annoying than anything, it doesnt work currently - removed piped because its more annoying than anything, it doesnt work currently
*shortest changelog wr*
> # 1.7.9.1 (2024-09) > # 1.7.9.1 (2024-09)

View file

@ -24,7 +24,7 @@ if (isBeta) {
android { android {
namespace 'com.deniscerri.ytdl' namespace 'com.deniscerri.ytdl'
compileSdk 34 compileSdk 35
try { try {
def propertiesFile = rootProject.file("keystore.properties") def propertiesFile = rootProject.file("keystore.properties")
@ -44,8 +44,8 @@ android {
}catch(IOException ignored){} }catch(IOException ignored){}
defaultConfig { defaultConfig {
applicationId "com.deniscerri.ytdl" applicationId "com.deniscerri.ytdl"
minSdk 23 minSdk 24
targetSdk 33 targetSdk 35
versionCode versionMajor * 1000000 + versionMinor * 10000 + versionPatch * 100 + versionBuild versionCode versionMajor * 1000000 + versionMinor * 10000 + versionPatch * 100 + versionBuild
versionName "${versionMajor}.${versionMinor}.${versionPatch}${versionExt}" versionName "${versionMajor}.${versionMinor}.${versionPatch}${versionExt}"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -135,14 +135,15 @@ android {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs_nio:2.1.0" coreLibraryDesugaring "com.android.tools:desugar_jdk_libs_nio:2.1.2"
implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer" // implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer" // implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer" // implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer"
// implementation "io.github.junkfood02.youtubedl-android:library:0.16.1"
// implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.16.1" implementation "io.github.junkfood02.youtubedl-android:library:0.17.1"
// implementation "io.github.junkfood02.youtubedl-android:aria2c:0.16.1" implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.17.1"
implementation "io.github.junkfood02.youtubedl-android:aria2c:0.17.1"
implementation "androidx.appcompat:appcompat:$appCompatVer" implementation "androidx.appcompat:appcompat:$appCompatVer"
implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.constraintlayout:constraintlayout:2.1.4"
@ -154,7 +155,7 @@ dependencies {
implementation "androidx.navigation:navigation-ui-ktx:$navVer" implementation "androidx.navigation:navigation-ui-ktx:$navVer"
implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.test.ext:junit-ktx:1.2.1' implementation 'androidx.test.ext:junit-ktx:1.2.1'
implementation 'androidx.compose.ui:ui-android:1.6.8' implementation 'androidx.compose.ui:ui-android:1.7.4'
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
testImplementation "junit:junit:$junitVer" testImplementation "junit:junit:$junitVer"
androidTestImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer"
@ -176,10 +177,10 @@ dependencies {
implementation "androidx.room:room-runtime:$roomVer" implementation "androidx.room:room-runtime:$roomVer"
implementation "androidx.room:room-ktx:$roomVer" implementation "androidx.room:room-ktx:$roomVer"
ksp "androidx.room:room-compiler:$roomVer" ksp "androidx.room:room-compiler:$roomVer"
implementation 'androidx.paging:paging-runtime-ktx:3.3.0' implementation 'androidx.paging:paging-runtime-ktx:3.3.2'
implementation "androidx.room:room-paging:$roomVer" implementation "androidx.room:room-paging:$roomVer"
androidTestImplementation "androidx.room:room-testing:$roomVer" androidTestImplementation "androidx.room:room-testing:$roomVer"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.6'
implementation "androidx.compose.runtime:runtime:$composeVer" implementation "androidx.compose.runtime:runtime:$composeVer"
androidTestImplementation 'com.google.truth:truth:1.1.5' androidTestImplementation 'com.google.truth:truth:1.1.5'
@ -191,7 +192,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4' implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.4" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.6"
implementation "com.github.Irineu333:Highlight-KT:1.0.4" implementation "com.github.Irineu333:Highlight-KT:1.0.4"
// For media playback using ExoPlayer // For media playback using ExoPlayer
@ -207,8 +208,8 @@ dependencies {
implementation "com.anggrayudi:storage:1.5.5" implementation "com.anggrayudi:storage:1.5.5"
implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'me.zhanghai.android.fastscroll:library:1.3.0'
implementation 'com.google.accompanist:accompanist-webview:0.30.1' implementation 'com.google.accompanist:accompanist-webview:0.30.1'
implementation 'androidx.compose.material3:material3-android:1.2.1' implementation 'androidx.compose.material3:material3-android:1.3.0'
implementation "io.noties.markwon:core:4.6.2" implementation "io.noties.markwon:core:4.6.2"
implementation("org.greenrobot:eventbus:3.3.1") implementation("org.greenrobot:eventbus:3.3.1")
implementation("com.github.teamnewpipe:NewPipeExtractor:0.24.2") implementation("com.github.teamnewpipe:newpipeextractor:v0.24.2")
} }

View file

@ -16,6 +16,7 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.util.Patterns
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import android.widget.CheckBox import android.widget.CheckBox
@ -51,6 +52,7 @@ import com.deniscerri.ytdl.ui.downloads.DownloadQueueMainFragment
import com.deniscerri.ytdl.ui.downloads.HistoryFragment import com.deniscerri.ytdl.ui.downloads.HistoryFragment
import com.deniscerri.ytdl.ui.more.settings.SettingsActivity import com.deniscerri.ytdl.ui.more.settings.SettingsActivity
import com.deniscerri.ytdl.util.CrashListener import com.deniscerri.ytdl.util.CrashListener
import com.deniscerri.ytdl.util.Extensions.extractURL
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NavbarUtil import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.NavbarUtil.applyNavBarStyle import com.deniscerri.ytdl.util.NavbarUtil.applyNavBarStyle
@ -183,7 +185,7 @@ class MainActivity : BaseActivity() {
(navigationView as NavigationBarView).getOrCreateBadge(R.id.historyFragment) (navigationView as NavigationBarView).getOrCreateBadge(R.id.historyFragment)
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.activeDownloadsCount.collectLatest { downloadViewModel.activePausedDownloadsCount.collectLatest {
if (it == 0) { if (it == 0) {
activeDownloadsBadge.isVisible = false activeDownloadsBadge.isVisible = false
activeDownloadsBadge.clearNumber() activeDownloadsBadge.clearNumber()
@ -438,7 +440,7 @@ class MainActivity : BaseActivity() {
textBuilder.append(c.toChar()) textBuilder.append(c.toChar())
} }
val bundle = Bundle() val bundle = Bundle()
bundle.putString("url", textBuilder.toString()) bundle.putString("url", textBuilder.toString().extractURL())
navController.popBackStack(R.id.homeFragment, true) navController.popBackStack(R.id.homeFragment, true)
navController.navigate( navController.navigate(
R.id.homeFragment, R.id.homeFragment,

View file

@ -26,6 +26,15 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Active'") @Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloads() : Flow<List<DownloadItem>> fun getActiveDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Active' or status = 'Paused' ORDER BY CASE WHEN status = 'Active' THEN 0 ELSE 1 END")
fun getActiveAndPausedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Paused'")
fun getPausedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Paused'")
fun getPausedDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status = 'Processing'") @Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloads() : Flow<List<DownloadItemConfigureMultiple>> fun getProcessingDownloads() : Flow<List<DownloadItemConfigureMultiple>>
@ -79,10 +88,11 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')") @Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')")
fun getActiveAndQueuedDownloadsList() : List<DownloadItem> fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@Query("UPDATE downloads SET status='Queued' where status = 'Active'")
suspend fun resetActiveToQueued()
@Query("SELECT id FROM downloads WHERE status in('Active','Queued')") @Query("UPDATE downloads SET status='Queued', downloadStartTime = -1 where status in ('Paused')")
suspend fun resetPausedToQueued()
@Query("SELECT id FROM downloads WHERE status in('Active','Queued', 'Paused')")
fun getActiveAndQueuedDownloadIDs() : List<Long> fun getActiveAndQueuedDownloadIDs() : List<Long>
@Query("SELECT * FROM downloads WHERE status in('Active','Queued')") @Query("SELECT * FROM downloads WHERE status in('Active','Queued')")
@ -95,7 +105,11 @@ interface DownloadDao {
@Query("SELECT format FROM downloads WHERE status='Queued'") @Query("SELECT format FROM downloads WHERE status='Queued'")
fun getSelectedFormatFromQueued() : List<Format> fun getSelectedFormatFromQueued() : List<Format>
@Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status in ('Queued', 'Scheduled') ORDER BY downloadStartTime, id LIMIT 20") @Query("""
SELECT * FROM downloads
WHERE downloadStartTime <= :currentTime and status in ('Queued', 'Scheduled')
ORDER BY downloadStartTime, id
""")
fun getQueuedScheduledDownloadsUntil(currentTime: Long) : Flow<List<DownloadItem>> fun getQueuedScheduledDownloadsUntil(currentTime: Long) : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id") @Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id")
@ -187,7 +201,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','Active', 'Scheduled')") @Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','Active', 'Scheduled', 'Paused')")
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")
@ -200,7 +214,11 @@ interface DownloadDao {
suspend fun updateAll(list: List<DownloadItem>) : List<DownloadItem> { suspend fun updateAll(list: List<DownloadItem>) : List<DownloadItem> {
val toReturn = mutableListOf<DownloadItem>() val toReturn = mutableListOf<DownloadItem>()
list.forEach { list.forEach {
it.id = update(it) if (it.id > 0) {
update(it)
}else{
it.id = insert(it)
}
toReturn.add(it) toReturn.add(it)
} }

View file

@ -11,23 +11,23 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface HistoryDao { interface HistoryDao {
@Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:format||'%' AND website LIKE '%'||:site||'%' ORDER BY " + @Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:type||'%' AND website LIKE '%'||:site||'%' ORDER BY " +
"CASE WHEN :sort = 'ASC' THEN id END ASC," + "CASE WHEN :sort = 'ASC' THEN id END ASC," +
"CASE WHEN :sort = 'DESC' THEN id END DESC," + "CASE WHEN :sort = 'DESC' THEN id END DESC," +
"CASE WHEN :sort = '' THEN id END DESC ") "CASE WHEN :sort = '' THEN id END DESC ")
fun getHistorySortedByID(query : String, format : String, site : String, sort : String) : List<HistoryItem> fun getHistorySortedByID(query : String, type : String, site : String, sort : String) : List<HistoryItem>
@Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:format||'%' AND website LIKE '%'||:site||'%' ORDER BY " + @Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:type||'%' AND website LIKE '%'||:site||'%' ORDER BY " +
"CASE WHEN :sort = 'ASC' THEN title END ASC," + "CASE WHEN :sort = 'ASC' THEN title END ASC," +
"CASE WHEN :sort = 'DESC' THEN title END DESC," + "CASE WHEN :sort = 'DESC' THEN title END DESC," +
"CASE WHEN :sort = '' THEN title END DESC ") "CASE WHEN :sort = '' THEN title END DESC ")
fun getHistorySortedByTitle(query : String, format : String, site : String, sort : String) : List<HistoryItem> fun getHistorySortedByTitle(query : String, type : String, site : String, sort : String) : List<HistoryItem>
@Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:format||'%' AND website LIKE '%'||:site||'%' ORDER BY " + @Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:type||'%' AND website LIKE '%'||:site||'%' ORDER BY " +
"CASE WHEN :sort = 'ASC' THEN author END ASC," + "CASE WHEN :sort = 'ASC' THEN author END ASC," +
"CASE WHEN :sort = 'DESC' THEN author END DESC," + "CASE WHEN :sort = 'DESC' THEN author END DESC," +
"CASE WHEN :sort = '' THEN author END DESC ") "CASE WHEN :sort = '' THEN author END DESC ")
fun getHistorySortedByAuthor(query : String, format : String, site : String, sort : String) : List<HistoryItem> fun getHistorySortedByAuthor(query : String, type : String, site : String, sort : String) : List<HistoryItem>
@Query("SELECT * FROM history") @Query("SELECT * FROM history")

View file

@ -44,6 +44,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
pagingSourceFactory = {downloadDao.getAllDownloads()} pagingSourceFactory = {downloadDao.getAllDownloads()}
) )
val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads().distinctUntilChanged() val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads().distinctUntilChanged()
val activePausedDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveAndPausedDownloads().distinctUntilChanged()
val pausedDownloads : Flow<List<DownloadItem>> = downloadDao.getPausedDownloads().distinctUntilChanged()
val processingDownloads : Flow<List<DownloadItemConfigureMultiple>> = downloadDao.getProcessingDownloads().distinctUntilChanged() val processingDownloads : Flow<List<DownloadItemConfigureMultiple>> = downloadDao.getProcessingDownloads().distinctUntilChanged()
val queuedDownloads : Pager<Int, DownloadItemSimple> = Pager( val queuedDownloads : Pager<Int, DownloadItemSimple> = Pager(
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
@ -67,15 +69,16 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
) )
val activeDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active).toListString()) val activeDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active).toListString())
val activePausedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.Paused).toListString())
val queuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Queued).toListString()) val queuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Queued).toListString())
val activeQueuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.Queued).toListString()) val pausedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Paused).toListString())
val cancelledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Cancelled).toListString()) val cancelledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Cancelled).toListString())
val erroredDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Error).toListString()) val erroredDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Error).toListString())
val savedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Saved).toListString()) val savedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Saved).toListString())
val scheduledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Scheduled).toListString()) val scheduledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Scheduled).toListString())
enum class Status { enum class Status {
Active, Queued, Error, Cancelled, Saved, Processing, Scheduled, Duplicate Active, Paused, Queued, Error, Cancelled, Saved, Processing, Scheduled, Duplicate
} }
suspend fun insert(item: DownloadItem) : Long { suspend fun insert(item: DownloadItem) : Long {
@ -222,48 +225,47 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) : Result<String> { fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, continueAfterPriorityItems: Boolean = true) : Result<String> {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true) val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workManager = WorkManager.getInstance(context) val workManager = WorkManager.getInstance(context)
val currentWork = workManager.getWorkInfosByTag("download").await() val inputData = Data.Builder()
if (currentWork.size == 0 || currentWork.none{ it.state == WorkInfo.State.RUNNING } || (queuedItems.isNotEmpty() && queuedItems[0].downloadStartTime != 0L)){ val currentTime = System.currentTimeMillis()
var delay = 0L
val currentTime = System.currentTimeMillis() if (queuedItems.isNotEmpty()){
var delay = 0L val earliestStart = queuedItems.minBy { it.downloadStartTime }
if (queuedItems.isNotEmpty()){ delay = if (earliestStart.downloadStartTime != 0L){
val earliestStart = queuedItems.minBy { it.downloadStartTime } earliestStart.downloadStartTime.minus(currentTime)
delay = if (earliestStart.downloadStartTime != 0L){ } else 0
earliestStart.downloadStartTime.minus(currentTime) if (delay <= 60000L) delay = 0L
} else 0 inputData.putLongArray("priority_item_ids", queuedItems.map { it.id }.toLongArray())
if (delay <= 60000L) delay = 0L
}
val useAlarmForScheduling = sharedPreferences.getBoolean("use_alarm_for_scheduling", false)
if (delay > 0L && useAlarmForScheduling) {
AlarmScheduler(context).scheduleAt(queuedItems.minBy { it.downloadStartTime }.downloadStartTime)
return Result.success("")
}
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")
.setConstraints(workConstraints.build())
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
.setInputData(inputData.build())
workManager.enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
} }
val useAlarmForScheduling = sharedPreferences.getBoolean("use_alarm_for_scheduling", false)
if (delay > 0L && useAlarmForScheduling) {
AlarmScheduler(context).scheduleAt(queuedItems.minBy { it.downloadStartTime }.downloadStartTime)
return Result.success("")
}
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
inputData.putBoolean("continue_after_priority_ids", continueAfterPriorityItems)
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")
.setConstraints(workConstraints.build())
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
.setInputData(inputData.build())
workManager.enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
val message = StringBuilder() val message = StringBuilder()
val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdl.database.repository
import com.deniscerri.ytdl.database.DBManager.SORTING import com.deniscerri.ytdl.database.DBManager.SORTING
import com.deniscerri.ytdl.database.dao.HistoryDao import com.deniscerri.ytdl.database.dao.HistoryDao
import com.deniscerri.ytdl.database.models.HistoryItem import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -26,41 +27,28 @@ class HistoryRepository(private val historyDao: HistoryDao) {
return historyDao.getAllHistoryByURL(url) return historyDao.getAllHistoryByURL(url)
} }
fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, notDeleted: Boolean) : List<HistoryItem> { fun getFiltered(query : String, type : String, site : String, sortType: HistorySortType, sort: SORTING, statusFilter: HistoryViewModel.HistoryStatus) : List<HistoryItem> {
var filtered = when(sortType){ var filtered = when(sortType){
HistorySortType.DATE -> historyDao.getHistorySortedByID(query, format, site, sort.toString()) HistorySortType.DATE -> historyDao.getHistorySortedByID(query, type, site, sort.toString())
HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, format, site, sort.toString()) HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, type, site, sort.toString())
HistorySortType.AUTHOR -> historyDao.getHistorySortedByAuthor(query, format, site, sort.toString()) HistorySortType.AUTHOR -> historyDao.getHistorySortedByAuthor(query, type, site, sort.toString())
HistorySortType.FILESIZE -> { HistorySortType.FILESIZE -> {
val items = historyDao.getHistorySortedByID(query, format, site, sort.toString()) val items = historyDao.getHistorySortedByID(query, type, site, sort.toString())
when(sort){ when(sort){
SORTING.DESC -> items.sortedByDescending { it.format.filesize } SORTING.DESC -> items.sortedByDescending { it.format.filesize }
SORTING.ASC -> items.sortedBy { it.format.filesize } SORTING.ASC -> items.sortedBy { it.format.filesize }
} }
} }
} }
if(notDeleted){
filtered = filtered.filter { it.downloadPath.any { it2 -> FileUtil.exists(it2) } }
}
return filtered when(statusFilter) {
} HistoryViewModel.HistoryStatus.DELETED -> {
filtered = filtered.filter { it.downloadPath.any { it2 -> !FileUtil.exists(it2) } }
fun getRecordsBetweenTwoItems(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, notDeleted: Boolean) : List<HistoryItem> {
var filtered = when(sortType){
HistorySortType.DATE -> historyDao.getHistorySortedByID(query, format, site, sort.toString())
HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, format, site, sort.toString())
HistorySortType.AUTHOR -> historyDao.getHistorySortedByAuthor(query, format, site, sort.toString())
HistorySortType.FILESIZE -> {
val items = historyDao.getHistorySortedByID(query, format, site, sort.toString())
when(sort){
SORTING.DESC -> items.sortedByDescending { it.format.filesize }
SORTING.ASC -> items.sortedBy { it.format.filesize }
}
} }
} HistoryViewModel.HistoryStatus.NOT_DELETED -> {
if(notDeleted){ filtered = filtered.filter { it.downloadPath.any { it2 -> FileUtil.exists(it2) } }
filtered = filtered.filter { it.downloadPath.any { it2 -> FileUtil.exists(it2) } } }
else -> {}
} }
return filtered return filtered

View file

@ -14,12 +14,14 @@ import com.deniscerri.ytdl.database.repository.HistoryRepository.HistorySortType
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.isYoutubeChannelURL import com.deniscerri.ytdl.util.Extensions.isYoutubeChannelURL
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.isYoutubeWatchVideosURL
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.extractors.GoogleApiUtil import com.deniscerri.ytdl.util.extractors.GoogleApiUtil
import com.deniscerri.ytdl.util.extractors.PipedApiUtil import com.deniscerri.ytdl.util.extractors.PipedApiUtil
import com.deniscerri.ytdl.util.extractors.newpipe.NewPipeUtil import com.deniscerri.ytdl.util.extractors.newpipe.NewPipeUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.deniscerri.ytdl.util.extractors.YoutubeApiUtil import com.deniscerri.ytdl.util.extractors.YoutubeApiUtil
import com.yausername.youtubedl_android.YoutubeDLException
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -46,6 +48,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
enum class SourceType { enum class SourceType {
YOUTUBE_VIDEO, YOUTUBE_VIDEO,
YOUTUBE_WATCHVIDEOS,
YOUTUBE_PLAYLIST, YOUTUBE_PLAYLIST,
YOUTUBE_CHANNEL, YOUTUBE_CHANNEL,
SEARCH_QUERY, SEARCH_QUERY,
@ -111,6 +114,40 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return items return items
} }
private suspend fun getYoutubeWatchVideos(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem> {
if (resetResults) deleteAll()
//throw YoutubeDLException("Youtube Watch Videos is not yet supported in data fetching. You can download it directly by clicking Continue Anyway or by Quick Downloading it!")
//TODO use below code after youtubedl-android has fixed issue #295 in their repo
val items = mutableListOf<ResultItem>()
val ytExtractorResult = newPipeUtil?.getPlaylistData(inputQuery) {
if (addToResults){
runBlocking {
val ids = resultDao.insertMultiple(it)
ids.forEachIndexed { index, id ->
it[index].id = id
}
}
}
items.addAll(it)
}
val response = if (ytExtractorResult?.isSuccess == true){
ytExtractorResult.getOrElse { items }
}else{
val res = ytdlpUtil.getFromYTDL(inputQuery)
if (addToResults) {
val ids = resultDao.insertMultiple(res)
ids.forEachIndexed { index, id ->
res[index].id = id
}
}
res
}
itemCount.value = response.size
return response
}
private suspend fun getYoutubeVideo(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{ private suspend fun getYoutubeVideo(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{
val theURL = inputQuery.replace("\\?list.*".toRegex(), "") val theURL = inputQuery.replace("\\?list.*".toRegex(), "")
val ytExtractorRes = newPipeUtil?.getVideoData(theURL) val ytExtractorRes = newPipeUtil?.getVideoData(theURL)
@ -157,9 +194,11 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
ytExtractorResult.getOrElse { items } ytExtractorResult.getOrElse { items }
}else{ }else{
val res = ytdlpUtil.getFromYTDL(playlistURL) val res = ytdlpUtil.getFromYTDL(playlistURL)
val ids = resultDao.insertMultiple(res) if (addToResults) {
ids.forEachIndexed { index, id -> val ids = resultDao.insertMultiple(res)
res[index].id = id ids.forEachIndexed { index, id ->
res[index].id = id
}
} }
res res
} }
@ -313,6 +352,9 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
SourceType.YOUTUBE_VIDEO -> { SourceType.YOUTUBE_VIDEO -> {
getYoutubeVideo(inputQuery, resetResults, addToResults) getYoutubeVideo(inputQuery, resetResults, addToResults)
} }
SourceType.YOUTUBE_WATCHVIDEOS -> {
getYoutubeWatchVideos(inputQuery, resetResults, addToResults)
}
SourceType.YOUTUBE_PLAYLIST -> { SourceType.YOUTUBE_PLAYLIST -> {
if (singleItem){ if (singleItem){
getFromYTDLP(inputQuery, resetResults, addToResults, true) getFromYTDLP(inputQuery, resetResults, addToResults, true)
@ -345,6 +387,8 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
type = SourceType.YOUTUBE_PLAYLIST type = SourceType.YOUTUBE_PLAYLIST
}else if (inputQuery.isYoutubeChannelURL()) { }else if (inputQuery.isYoutubeChannelURL()) {
type = SourceType.YOUTUBE_CHANNEL type = SourceType.YOUTUBE_CHANNEL
}else if (inputQuery.isYoutubeWatchVideosURL()) {
type = SourceType.YOUTUBE_WATCHVIDEOS
} }
} else if (Patterns.WEB_URL.matcher(inputQuery).matches()) { } else if (Patterns.WEB_URL.matcher(inputQuery).matches()) {
type = SourceType.YT_DLP type = SourceType.YT_DLP

View file

@ -9,9 +9,13 @@ import android.os.Parcelable
import android.util.DisplayMetrics import android.util.DisplayMetrics
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.filter
import androidx.paging.map
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.Data import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
@ -38,16 +42,23 @@ import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple
import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatUtil import com.deniscerri.ytdl.util.FormatUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker
import com.google.gson.Gson import com.google.gson.Gson
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CancellationException 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.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -62,12 +73,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao private val commandTemplateDao: CommandTemplateDao
private val formatUtil = FormatUtil(application) private val formatUtil = FormatUtil(application)
private val notificationUtil = NotificationUtil(application)
private val ytdlpUtil = YTDLPUtil(application) private val ytdlpUtil = YTDLPUtil(application)
private val resources : Resources private val resources : Resources
val allDownloads : Flow<PagingData<DownloadItem>> val allDownloads : Flow<PagingData<DownloadItem>>
val queuedDownloads : Flow<PagingData<DownloadItemSimple>> val queuedDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloads : Flow<List<DownloadItem>> val activeDownloads : Flow<List<DownloadItem>>
val activePausedDownloads : Flow<List<DownloadItem>>
val processingDownloads : Flow<List<DownloadItemConfigureMultiple>> val processingDownloads : Flow<List<DownloadItemConfigureMultiple>>
val cancelledDownloads : Flow<PagingData<DownloadItemSimple>> val cancelledDownloads : Flow<PagingData<DownloadItemSimple>>
val erroredDownloads : Flow<PagingData<DownloadItemSimple>> val erroredDownloads : Flow<PagingData<DownloadItemSimple>>
@ -75,13 +88,21 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val scheduledDownloads : Flow<PagingData<DownloadItemSimple>> val scheduledDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloadsCount : Flow<Int> val activeDownloadsCount : Flow<Int>
val activePausedDownloadsCount : Flow<Int>
val queuedDownloadsCount : Flow<Int> val queuedDownloadsCount : Flow<Int>
val activeQueuedDownloadsCount : Flow<Int> val pausedDownloadsCount : Flow<Int>
val cancelledDownloadsCount : Flow<Int> val cancelledDownloadsCount : Flow<Int>
val erroredDownloadsCount : Flow<Int> val erroredDownloadsCount : Flow<Int>
val savedDownloadsCount : Flow<Int> val savedDownloadsCount : Flow<Int>
val scheduledDownloadsCount : Flow<Int> val scheduledDownloadsCount : Flow<Int>
val pausedAllDownloads = MediatorLiveData(PausedAllDownloadsState.HIDDEN)
private val pausedAllDownloadsFlow : Flow<PausedAllDownloadsState>
private var isPausingResuming = false
enum class PausedAllDownloadsState {
PAUSE, RESUME, PROCESSING, HIDDEN
}
@Parcelize @Parcelize
data class AlreadyExistsIDs( data class AlreadyExistsIDs(
var downloadItemID: Long, var downloadItemID: Long,
@ -122,8 +143,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
activeDownloadsCount = repository.activeDownloadsCount activeDownloadsCount = repository.activeDownloadsCount
activePausedDownloadsCount = repository.activePausedDownloadsCount
queuedDownloadsCount = repository.queuedDownloadsCount queuedDownloadsCount = repository.queuedDownloadsCount
activeQueuedDownloadsCount = repository.activeQueuedDownloadsCount pausedDownloadsCount = repository.pausedDownloadsCount
cancelledDownloadsCount = repository.cancelledDownloadsCount cancelledDownloadsCount = repository.cancelledDownloadsCount
erroredDownloadsCount = repository.erroredDownloadsCount erroredDownloadsCount = repository.erroredDownloadsCount
savedDownloadsCount = repository.savedDownloadsCount savedDownloadsCount = repository.savedDownloadsCount
@ -132,6 +154,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
allDownloads = repository.allDownloads.flow allDownloads = repository.allDownloads.flow
queuedDownloads = repository.queuedDownloads.flow queuedDownloads = repository.queuedDownloads.flow
activeDownloads = repository.activeDownloads activeDownloads = repository.activeDownloads
activePausedDownloads = repository.activePausedDownloads
processingDownloads = repository.processingDownloads processingDownloads = repository.processingDownloads
savedDownloads = repository.savedDownloads.flow savedDownloads = repository.savedDownloads.flow
scheduledDownloads = repository.scheduledDownloads.flow scheduledDownloads = repository.scheduledDownloads.flow
@ -144,6 +167,25 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
} }
pausedAllDownloadsFlow = combine(activeDownloadsCount, queuedDownloadsCount, pausedDownloadsCount) { active, queued, paused ->
if (isPausingResuming) {
return@combine PausedAllDownloadsState.PROCESSING
}
if (active == 0 && queued == 0 && paused == 0) {
return@combine PausedAllDownloadsState.HIDDEN
}else if (paused > 1 || (active == 0 && queued > 0) || (paused > 0 && active > 0)) {
return@combine PausedAllDownloadsState.RESUME
}else if (active > 1) {
return@combine PausedAllDownloadsState.PAUSE
}else{
return@combine PausedAllDownloadsState.HIDDEN
}
}
pausedAllDownloads.addSource(pausedAllDownloadsFlow.asLiveData()) {
pausedAllDownloads.value = it
}
val confTmp = Configuration(application.resources.configuration) val confTmp = Configuration(application.resources.configuration)
val locale = if (Build.VERSION.SDK_INT < 33) { val locale = if (Build.VERSION.SDK_INT < 33) {
@ -496,13 +538,20 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
) )
} }
else -> { else -> {
val lastUsedCommandTemplate = sharedPreferences.getString("lastCommandTemplateUsed", "")!! val preferredTemplateContent =
val c = if (lastUsedCommandTemplate.isBlank()){ sharedPreferences.getString("preferred_command_template", "")!!.ifEmpty {
commandTemplateDao.getFirst() ?: CommandTemplate(0,"","", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false) sharedPreferences.getString("lastCommandTemplateUsed", "")!!
}else{ }
commandTemplateDao.getTemplateByContent(lastUsedCommandTemplate) ?: CommandTemplate(0, "", lastUsedCommandTemplate, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
val c = if (preferredTemplateContent.isNotBlank()) {
commandTemplateDao.getTemplateByContent(preferredTemplateContent)
}else {
commandTemplateDao.getFirst()
} }
return generateCommandFormat(c)
return generateCommandFormat(
c ?: CommandTemplate(0, "", preferredTemplateContent, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
)
} }
} }
} }
@ -656,7 +705,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.deleteAllWithIDs(ids) repository.deleteAllWithIDs(ids)
} }
fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) { private fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) {
processingItemsJob?.apply { cancel(CancellationException()) } processingItemsJob?.apply { cancel(CancellationException()) }
repository.cancelActiveQueued() repository.cancelActiveQueued()
} }
@ -705,14 +754,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.startDownloadWorker(emptyList(), application) repository.startDownloadWorker(emptyList(), application)
} }
suspend fun resetActiveToQueued() {
dbManager.downloadDao.resetActiveToQueued()
}
suspend fun startDownloadWorker(list: List<DownloadItem>){
repository.startDownloadWorker(list, application)
}
suspend fun putAtTopOfQueue(ids: List<Long>) = CoroutineScope(Dispatchers.IO).launch{ suspend fun putAtTopOfQueue(ids: List<Long>) = CoroutineScope(Dispatchers.IO).launch{
val downloads = dao.getQueuedDownloadsListIDs() val downloads = dao.getQueuedDownloadsListIDs()
val lastID = ids.maxOf { it } val lastID = ids.maxOf { it }
@ -809,11 +850,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
items.forEach { items.forEach {
if (it.downloadStartTime > 0) { if (it.downloadStartTime > 0) {
it.status = DownloadRepository.Status.Scheduled.toString() it.status = DownloadRepository.Status.Scheduled.toString()
}else{ }else {
it.status = DownloadRepository.Status.Queued.toString() it.status = DownloadRepository.Status.Queued.toString()
} }
//CHECK DUPLICATES //CHECK DUPLICATES
var isDuplicate = false var isDuplicate = false
if (checkDuplicate.isNotEmpty() && !ignoreDuplicates){ if (checkDuplicate.isNotEmpty() && !ignoreDuplicates){
@ -939,11 +979,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
}else{ }else{
val queued = repository.updateAll(queuedItems) val queued = repository.updateAll(queuedItems)
println(queued.size)
if (sharedPreferences.getBoolean("paused_downloads", true)) {
sharedPreferences.edit().putBoolean("paused_downloads", false).apply()
resetActiveToQueued()
}
result.message = repository.startDownloadWorker(queued, context).getOrElse { "" } result.message = repository.startDownloadWorker(queued, context).getOrElse { "" }
@ -1173,4 +1209,69 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun pauseAllDownloads() = viewModelScope.launch {
pausedAllDownloads.value = PausedAllDownloadsState.PROCESSING
isPausingResuming = true
WorkManager.getInstance(application).cancelAllWorkByTag("download")
val activeDownloadsList = withContext(Dispatchers.IO){
getActiveDownloads()
}
activeDownloadsList.forEach {
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt())
}
delay(1000)
isPausingResuming = false
activeDownloadsList.forEach {
repository.setDownloadStatus(it.id, DownloadRepository.Status.Paused)
}
pausedAllDownloads.value = PausedAllDownloadsState.RESUME
}
fun resumeAllDownloads() = viewModelScope.launch {
pausedAllDownloads.value = PausedAllDownloadsState.PROCESSING
isPausingResuming = true
WorkManager.getInstance(application).cancelAllWorkByTag("download")
val paused = withContext(Dispatchers.IO) {
dao.getPausedDownloadsList()
}
withContext(Dispatchers.IO){
dbManager.downloadDao.resetPausedToQueued()
repository.startDownloadWorker(paused, application)
}
delay(1000)
isPausingResuming = false
withContext(Dispatchers.Main) {
pausedAllDownloads.value = PausedAllDownloadsState.PAUSE
}
}
fun cancelAllDownloads() = viewModelScope.launch {
WorkManager.getInstance(application).cancelAllWorkByTag("download")
val activeAndQueued = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloadIDs()
}
activeAndQueued.forEach { id ->
YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id.toInt())
}
cancelActiveQueued()
}
fun resumeDownload(itemID: Long) = viewModelScope.launch {
kotlin.runCatching {
val item = withContext(Dispatchers.IO){
repository.getItemByID(itemID)
}
item.status = DownloadRepository.Status.Queued.toString()
withContext(Dispatchers.IO){
updateDownload(item)
}
putAtTopOfQueue(listOf(itemID))
withContext(Dispatchers.IO){
repository.startDownloadWorker(listOf(item), application, false)
}
}
}
} }

View file

@ -10,9 +10,17 @@ import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.DBManager.SORTING import com.deniscerri.ytdl.database.DBManager.SORTING
import com.deniscerri.ytdl.database.models.HistoryItem import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository.HistorySortType import com.deniscerri.ytdl.database.repository.HistoryRepository.HistorySortType
import com.deniscerri.ytdl.util.Extensions
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class HistoryViewModel(application: Application) : AndroidViewModel(application) { class HistoryViewModel(application: Application) : AndroidViewModel(application) {
@ -20,36 +28,40 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
val sortOrder = MutableLiveData(SORTING.DESC) val sortOrder = MutableLiveData(SORTING.DESC)
val sortType = MutableLiveData(HistorySortType.DATE) val sortType = MutableLiveData(HistorySortType.DATE)
val websiteFilter = MutableLiveData("") val websiteFilter = MutableLiveData("")
val statusFilter = MutableLiveData(HistoryStatus.UNSET)
private val queryFilter = MutableLiveData("") private val queryFilter = MutableLiveData("")
private val formatFilter = MutableLiveData("") private val typeFilter = MutableLiveData("")
private val notDeletedFilter = MutableLiveData(false)
enum class HistoryStatus {
UNSET, DELETED, NOT_DELETED, ALL
}
val allItems : LiveData<List<HistoryItem>> val allItems : LiveData<List<HistoryItem>>
private var _items = MediatorLiveData<List<HistoryItem>>() private var _items = MediatorLiveData<List<HistoryItem>>()
init { init {
val dao = DBManager.getInstance(application).historyDao val dao = DBManager.getInstance(application).historyDao
repository = HistoryRepository(dao) repository = HistoryRepository(dao)
allItems = repository.items.asLiveData() allItems = repository.items.asLiveData()
_items.addSource(allItems){ _items.addSource(allItems){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!) filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
} }
_items.addSource(formatFilter){ _items.addSource(typeFilter){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!) filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
} }
_items.addSource(sortType){ _items.addSource(sortType){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!) filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
} }
_items.addSource(websiteFilter){ _items.addSource(websiteFilter){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!) filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
} }
_items.addSource(queryFilter){ _items.addSource(queryFilter){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!) filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
} }
_items.addSource(notDeletedFilter){ _items.addSource(statusFilter){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!) filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
} }
} }
@ -77,22 +89,26 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
queryFilter.value = filter queryFilter.value = filter
} }
fun setFormatFilter(filter: String){ fun setTypeFilter(filter: String){
formatFilter.value = filter typeFilter.value = filter
} }
fun setNotDeleted(filter: Boolean){ fun setStatusFilter(status: HistoryStatus) {
notDeletedFilter.value = filter statusFilter.value = status
} }
private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, notDeleted: Boolean) = viewModelScope.launch(Dispatchers.IO){ private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, statusFilter: HistoryStatus) = viewModelScope.launch(Dispatchers.IO){
_items.postValue(repository.getFiltered(query, format, site, sortType, sort, notDeleted)) _items.postValue(repository.getFiltered(query, format, site, sortType, sort, statusFilter))
} }
fun getAll() : List<HistoryItem> { fun getAll() : List<HistoryItem> {
return repository.getAll() return repository.getAll()
} }
fun getByID(id: Long) : HistoryItem {
return repository.getItem(id)
}
fun insert(item: HistoryItem) = viewModelScope.launch(Dispatchers.IO){ fun insert(item: HistoryItem) = viewModelScope.launch(Dispatchers.IO){
repository.insert(item) repository.insert(item)
} }
@ -118,13 +134,14 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
} }
fun getRecordsBetweenTwoItems(item1: Long, item2: Long) : List<HistoryItem> { fun getRecordsBetweenTwoItems(item1: Long, item2: Long) : List<HistoryItem> {
val filtered = repository.getFiltered(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!) val filtered = repository.getFiltered(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
val firstIndex = filtered.indexOfFirst { it.id == item1 } val firstIndex = filtered.indexOfFirst { it.id == item1 }
val secondIndex = filtered.indexOfFirst { it.id == item2 } val secondIndex = filtered.indexOfFirst { it.id == item2 }
if(firstIndex > secondIndex) {
return filtered.filterIndexed { index, _ -> index in (secondIndex + 1) until firstIndex } return if(firstIndex > secondIndex) {
filtered.filterIndexed { index, _ -> index in (secondIndex + 1) until firstIndex }
}else{ }else{
return filtered.filterIndexed { index, _ -> index in (firstIndex + 1) until secondIndex } filtered.filterIndexed { index, _ -> index in (firstIndex + 1) until secondIndex }
} }
} }

View file

@ -33,6 +33,7 @@ import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
@ -96,6 +97,14 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
playlistFilter.value = p playlistFilter.value = p
} }
private fun resetPlaylistFilter() = viewModelScope.launch(Dispatchers.Main) {
_items.removeSource(playlistFilter)
playlistFilter.value = ""
_items.addSource(playlistFilter) {
filter()
}
}
fun getFilteredList() : LiveData<List<ResultItem>> { fun getFilteredList() : LiveData<List<ResultItem>> {
return _items return _items
} }
@ -133,7 +142,6 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
repository.itemCount.value = inputQueries.size repository.itemCount.value = inputQueries.size
} }
val resetResults = inputQueries.size == 1 val resetResults = inputQueries.size == 1
uiState.update {it.copy(processing = true, errorMessage = null)} uiState.update {it.copy(processing = true, errorMessage = null)}
val res = mutableListOf<ResultItem?>() val res = mutableListOf<ResultItem?>()
@ -195,6 +203,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
suspend fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { suspend fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll() repository.deleteAll()
resetPlaylistFilter()
} }
fun update(item: ResultItem) = viewModelScope.launch(Dispatchers.IO){ fun update(item: ResultItem) = viewModelScope.launch(Dispatchers.IO){

View file

@ -39,6 +39,7 @@ 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.ui.BaseActivity import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.util.Extensions.extractURL
import com.deniscerri.ytdl.util.ThemeUtil import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -170,16 +171,12 @@ class ShareActivity : BaseActivity() {
runCatching { supportFragmentManager.popBackStack() } runCatching { supportFragmentManager.popBackStack() }
quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false) || sharedPreferences.getString("preferred_download_type", "video") == "command") quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false) || sharedPreferences.getString("preferred_download_type", "video") == "command")
val url = when(action){ val data = when(action){
Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)!! Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)!!
else -> intent.dataString!! else -> intent.dataString!!
} }
val matcher = Patterns.WEB_URL.matcher(url)
val inputQuery = if (matcher.find()){ val inputQuery = data.extractURL()
matcher.group()
}else{
url
}
val type = intent.getStringExtra("TYPE") val type = intent.getStringExtra("TYPE")
val background = intent.getBooleanExtra("BACKGROUND", false) val background = intent.getBooleanExtra("BACKGROUND", false)

View file

@ -981,12 +981,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
searchView!!.editText.setSelection(searchView!!.editText.length()) searchView!!.editText.setSelection(searchView!!.editText.length())
} }
private fun updateMultiplePlaylistResults(playlistTitles: List<String>) { private fun updateMultiplePlaylistResults(playlistTitles: List<String>) {
playlistNameFilterChipGroup.children.filter { it.tag != "all" }.forEach {
playlistNameFilterChipGroup.removeView(it)
}
if (playlistTitles.isEmpty() || playlistTitles.size == 1) { if (playlistTitles.isEmpty() || playlistTitles.size == 1) {
playlistNameFilterScrollView.isVisible = false playlistNameFilterScrollView.isVisible = false
playlistNameFilterChipGroup.children.filter { it.tag != "all" }.forEach {
playlistNameFilterChipGroup.removeView(it)
}
return return
} }
@ -995,9 +997,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} }
for (t in playlistTitles) { for (t in playlistTitles) {
val exists = playlistNameFilterChipGroup.children.any { it.tag == t }
if (exists) continue
val tmp = layoutinflater!!.inflate(R.layout.filter_chip, playlistNameFilterChipGroup, false) as Chip val tmp = layoutinflater!!.inflate(R.layout.filter_chip, playlistNameFilterChipGroup, false) as Chip
tmp.text = t tmp.text = t
tmp.tag = t tmp.tag = t
@ -1008,6 +1007,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
playlistNameFilterChipGroup.addView(tmp) playlistNameFilterChipGroup.addView(tmp)
} }
if (playlistNameFilterChipGroup.children.all { !(it as Chip).isChecked }) {
(playlistNameFilterChipGroup.children.first() as Chip).isChecked = true
}
playlistNameFilterScrollView.isVisible = true playlistNameFilterScrollView.isVisible = true
} }
} }

View file

@ -71,8 +71,6 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
// PROGRESS BAR ---------------------------------------------------- // PROGRESS BAR ----------------------------------------------------
val progressBar = card.findViewById<LinearProgressIndicator>(R.id.progress) val progressBar = card.findViewById<LinearProgressIndicator>(R.id.progress)
progressBar.tag = "${item.id}##progress" progressBar.tag = "${item.id}##progress"
progressBar.progress = 0
progressBar.isIndeterminate = true
// TITLE ---------------------------------- // TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.title) val itemTitle = card.findViewById<TextView>(R.id.title)
@ -122,39 +120,39 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null) if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null)
cancelButton.setOnClickListener {onItemClickListener.onCancelClick(item.id)} cancelButton.setOnClickListener {onItemClickListener.onCancelClick(item.id)}
// val resumeButton = card.findViewById<MaterialButton>(R.id.active_download_resume) val resumeButton = card.findViewById<MaterialButton>(R.id.active_download_resume)
// resumeButton.isEnabled = true resumeButton.isEnabled = true
// if (resumeButton.hasOnClickListeners()) resumeButton.setOnClickListener(null) if (resumeButton.hasOnClickListeners()) resumeButton.setOnClickListener(null)
// if (activePaused) { val isPaused = item.status == DownloadRepository.Status.Paused.toString()
// resumeButton.setIconResource(R.drawable.exomedia_ic_play_arrow_white) if (isPaused) {
// resumeButton.setOnClickListener { resumeButton.setIconResource(R.drawable.exomedia_ic_play_arrow_white)
// resumeButton.isEnabled = false resumeButton.setOnClickListener {
// onItemClickListener.onResumeClick(item.id) resumeButton.isEnabled = false
// } onItemClickListener.onResumeClick(item.id)
// }else { }
// resumeButton.setIconResource(R.drawable.exomedia_ic_pause_white) }else {
// resumeButton.setOnClickListener { resumeButton.setIconResource(R.drawable.exomedia_ic_pause_white)
// resumeButton.isEnabled = false resumeButton.setOnClickListener {
// onItemClickListener.onPauseClick(item.id) resumeButton.isEnabled = false
// } onItemClickListener.onPauseClick(item.id)
// } }
}
if (sharedPreferences.getBoolean("paused_downloads", false)) { if (isPaused) {
progressBar.isIndeterminate = false progressBar.isIndeterminate = false
progressBar.progress = 0
cancelButton.isEnabled = true cancelButton.isEnabled = true
output.text = activity.getString(R.string.exo_download_paused) output.text = activity.getString(R.string.exo_download_paused)
}else{ }else{
progressBar.isIndeterminate = true progressBar.isIndeterminate = progressBar.progress <= 0
cancelButton.isEnabled = true cancelButton.isEnabled = true
} }
} }
interface OnItemClickListener { interface OnItemClickListener {
fun onCancelClick(itemID: Long) fun onCancelClick(itemID: Long)
fun onOutputClick(item: DownloadItem) fun onOutputClick(item: DownloadItem)
} fun onPauseClick(itemID: Long)
fun onResumeClick(itemID: Long)
enum class ActiveDownloadAction {
Resume, Pause
} }
companion object { companion object {

View file

@ -129,35 +129,35 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
else fileSize.text = fileSizeReadable else fileSize.text = fileSizeReadable
val menu = card.findViewById<View>(R.id.options) val menu = card.findViewById<View>(R.id.options)
//val paused = sharedPreferences.getBoolean("paused_downloads", false) || item.status == DownloadRepository.Status.ActivePaused.toString() val paused = item.status == DownloadRepository.Status.Paused.toString()
menu.setOnClickListener { menu.setOnClickListener {
val popup = PopupMenu(activity, it) val popup = PopupMenu(activity, it)
popup.menuInflater.inflate(R.menu.active_downloads_minified, popup.menu) popup.menuInflater.inflate(R.menu.active_downloads_minified, popup.menu)
if (Build.VERSION.SDK_INT > 27) popup.menu.setGroupDividerEnabled(true) if (Build.VERSION.SDK_INT > 27) popup.menu.setGroupDividerEnabled(true)
// val pause = popup.menu[0] val pause = popup.menu[0]
// val resume = popup.menu[1] val resume = popup.menu[1]
//
// if (paused){ if (paused){
// pause.isVisible = false pause.isVisible = false
// resume.isVisible = true resume.isVisible = true
// }else{ }else{
// pause.isVisible = true pause.isVisible = true
// resume.isVisible = false resume.isVisible = false
// } }
popup.setOnMenuItemClickListener { m -> popup.setOnMenuItemClickListener { m ->
when(m.itemId){ when(m.itemId){
// R.id.pause -> { R.id.pause -> {
// onItemClickListener.onPauseClick(item.id, ActiveDownloadAdapter.ActiveDownloadAction.Pause, position) onItemClickListener.onPauseClick(item.id, position)
// if (progressBar.progress == 0) progressBar.isIndeterminate = false if (progressBar.progress == 0) progressBar.isIndeterminate = false
// popup.dismiss() popup.dismiss()
// } }
// R.id.resume -> { R.id.resume -> {
// onItemClickListener.onPauseClick(item.id, ActiveDownloadAdapter.ActiveDownloadAction.Resume, position) onItemClickListener.onResumeClick(item.id, position)
// progressBar.isIndeterminate = true progressBar.isIndeterminate = true
// popup.dismiss() popup.dismiss()
// } }
R.id.cancel -> { R.id.cancel -> {
onItemClickListener.onCancelClick(item.id) onItemClickListener.onCancelClick(item.id)
popup.dismiss() popup.dismiss()
@ -170,7 +170,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
} }
progressBar.isIndeterminate = !sharedPreferences.getBoolean("paused_downloads", false) progressBar.isIndeterminate = !paused
card.setOnClickListener { card.setOnClickListener {
onItemClickListener.onCardClick() onItemClickListener.onCardClick()
@ -178,6 +178,8 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
} }
interface OnItemClickListener { interface OnItemClickListener {
fun onCancelClick(itemID: Long) fun onCancelClick(itemID: Long)
fun onPauseClick(itemID: Long, position: Int)
fun onResumeClick(itemID: Long, position: Int)
fun onCardClick() fun onCardClick()
} }

View file

@ -2,11 +2,16 @@ package com.deniscerri.ytdl.ui.adapter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.graphics.Color
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
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
@ -22,10 +27,12 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ
private val onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener
private val activity: Activity private val activity: Activity
private val checkedItems: ArrayList<Long> = ArrayList() private val checkedItems: ArrayList<Long> = ArrayList()
private val sharedPreferences: SharedPreferences
init { init {
this.onItemClickListener = onItemClickListener this.onItemClickListener = onItemClickListener
this.activity = activity this.activity = activity
this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
} }
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
@ -43,18 +50,18 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)!!
val card = holder.item val card = holder.item
card.popup() card.popup()
val title = card.findViewById<TextView>(R.id.title) val title = card.findViewById<TextView>(R.id.title)
title.text = item?.title title.text = item.title
val content = card.findViewById<TextView>(R.id.content) val content = card.findViewById<TextView>(R.id.content)
content.text = item?.content content.text = item.content
card.findViewById<TextView>(R.id.useInExtraCommands).apply { card.findViewById<TextView>(R.id.useInExtraCommands).apply {
isVisible = item!!.useAsExtraCommand isVisible = item.useAsExtraCommand
val extraAudio = if (item.useAsExtraCommandAudio) context.getString(R.string.audio) else null val extraAudio = if (item.useAsExtraCommandAudio) context.getString(R.string.audio) else null
val extraVideo = if (item.useAsExtraCommandVideo) context.getString(R.string.video) else null val extraVideo = if (item.useAsExtraCommandVideo) context.getString(R.string.video) else null
val finalText = context.getString(R.string.extra_command) + " " + listOfNotNull( val finalText = context.getString(R.string.extra_command) + " " + listOfNotNull(
@ -64,7 +71,12 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ
text = finalText text = finalText
} }
if (checkedItems.contains(item!!.id)) { card.findViewById<TextView>(R.id.preferredTemplate).apply {
val preferred = sharedPreferences.getString("preferred_command_template", "")
isVisible = preferred == item.content
}
if (checkedItems.contains(item.id)) {
card.isChecked = true card.isChecked = true
card.strokeWidth = 5 card.strokeWidth = 5
} else { } else {

View file

@ -20,8 +20,10 @@ import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
@ -240,16 +242,21 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
true true
} }
downloadThumb.isVisible = item.thumb.isNotBlank()
downloadThumb.setOnClickListener { downloadThumb.setOnClickListener {
downloadManager.enqueue( runCatching {
DownloadManager.Request(item.thumb.toUri()) downloadManager.enqueue(
.setAllowedNetworkTypes( DownloadManager.Request(item.thumb.toUri())
DownloadManager.Request.NETWORK_WIFI or .setAllowedNetworkTypes(
DownloadManager.Request.NETWORK_MOBILE DownloadManager.Request.NETWORK_WIFI or
) DownloadManager.Request.NETWORK_MOBILE
.setAllowedOverRoaming(true) )
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) .setAllowedOverRoaming(true)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/" + item.title + ".jpg")) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/" + item.title + ".jpg"))
}.onFailure {
Toast.makeText(requireContext(), getString(R.string.failed_download), Toast.LENGTH_SHORT).show()
}
} }
title.text = item.title title.text = item.title
@ -479,33 +486,24 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item) downloadViewModel.updateDownload(item)
} }
val activeCount = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloadsCount()
}
if (activeCount == 0){
val queuedCount = withContext(Dispatchers.IO){
downloadViewModel.getQueuedDownloadsCount()
}
if (queuedCount == 0) {
sharedPreferences.edit().putBoolean("paused_downloads", false).apply()
}
}
if (activeCount == 1){
val queue = withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}
if (!sharedPreferences.getBoolean("paused_downloads", false)){
runBlocking {
downloadViewModel.queueDownloads(queue)
}
}
}
} }
} }
override fun onPauseClick(itemID: Long, position: Int) {
lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
withContext(Dispatchers.IO){
downloadViewModel.updateToStatus(itemID, DownloadRepository.Status.Paused)
}
activeAdapter.notifyItemChanged(position)
}
}
override fun onResumeClick(itemID: Long, position: Int) {
downloadViewModel.resumeDownload(itemID)
}
override fun onCardClick() { override fun onCardClick() {
this.dismiss() this.dismiss()
findNavController().navigate( findNavController().navigate(

View file

@ -5,9 +5,11 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.ColorStateList
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.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -23,6 +25,8 @@ import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginBottom import androidx.core.view.marginBottom
@ -36,6 +40,7 @@ import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.room.util.getColumnIndexOrThrow
import androidx.work.WorkManager import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
@ -86,8 +91,8 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
private lateinit var activeDownloads : ActiveDownloadAdapter private lateinit var activeDownloads : ActiveDownloadAdapter
private lateinit var notificationUtil: NotificationUtil private lateinit var notificationUtil: NotificationUtil
private lateinit var pause: ExtendedFloatingActionButton private lateinit var pauseBtn: ExtendedFloatingActionButton
private lateinit var resume: ExtendedFloatingActionButton private lateinit var resumeBtn: ExtendedFloatingActionButton
private lateinit var noResults: RelativeLayout private lateinit var noResults: RelativeLayout
private lateinit var workManager: WorkManager private lateinit var workManager: WorkManager
private lateinit var preferences: SharedPreferences private lateinit var preferences: SharedPreferences
@ -119,80 +124,64 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
activeRecyclerView.adapter = activeDownloads activeRecyclerView.adapter = activeDownloads
activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
pause = view.findViewById(R.id.pause) pauseBtn = view.findViewById(R.id.pause)
pause.isVisible = false pauseBtn.isVisible = false
resume = view.findViewById(R.id.resume) resumeBtn = view.findViewById(R.id.resume)
noResults = view.findViewById(R.id.no_results) noResults = view.findViewById(R.id.no_results)
pause.setOnClickListener { downloadViewModel.pausedAllDownloads.observe(viewLifecycleOwner) { value ->
lifecycleScope.launch { lifecycleScope.launch {
pause.isEnabled = false when(value) {
delay(1000) DownloadViewModel.PausedAllDownloadsState.PAUSE -> {
workManager.cancelAllWorkByTag("download") resumeBtn.isVisible = false
val activeDownloadsList = withContext(Dispatchers.IO){ pauseBtn.isClickable = true
downloadViewModel.getActiveDownloads() val typedValue = TypedValue()
} requireContext().theme.resolveAttribute(R.attr.colorPrimaryContainer, typedValue, true)
pauseBtn.backgroundTintList = ColorStateList.valueOf(typedValue.data)
activeDownloadsList.forEach { pauseBtn.isVisible = true
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt())
}
preferences.edit().putBoolean("paused_downloads", true).apply()
pause.isVisible = false
resume.isEnabled = false
resume.isVisible = true
activeDownloads.notifyDataSetChanged()
resume.isEnabled = true
}
}
resume.setOnClickListener {
lifecycleScope.launch {
resume.isEnabled = false
delay(1000)
preferences.edit().putBoolean("paused_downloads", false).apply()
resume.isVisible = false
pause.isEnabled = false
pause.isVisible = true
withContext(Dispatchers.IO) {
downloadViewModel.resetActiveToQueued()
downloadViewModel.startDownloadWorker(listOf())
withContext(Dispatchers.Main){
activeDownloads.notifyDataSetChanged() activeDownloads.notifyDataSetChanged()
pause.isEnabled = true }
DownloadViewModel.PausedAllDownloadsState.RESUME -> {
pauseBtn.isVisible = false
resumeBtn.isClickable = true
val typedValue = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorPrimaryContainer, typedValue, true)
resumeBtn.backgroundTintList = ColorStateList.valueOf(typedValue.data)
resumeBtn.isVisible = true
activeDownloads.notifyDataSetChanged()
}
DownloadViewModel.PausedAllDownloadsState.PROCESSING -> {
pauseBtn.isClickable = false
pauseBtn.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.grey))
resumeBtn.isClickable = false
resumeBtn.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.grey))
}
else -> {
pauseBtn.isVisible = false
resumeBtn.isVisible = false
} }
} }
} }
} }
lifecycleScope.launch { pauseBtn.setOnClickListener {
val activeCount = withContext(Dispatchers.IO) { lifecycleScope.launch {
downloadViewModel.getActiveDownloadsCount() downloadViewModel.pauseAllDownloads()
} }
val queuedCount = withContext(Dispatchers.IO) {
downloadViewModel.getQueuedDownloadsCount()
}
val pausedDownloads = preferences.getBoolean("paused_downloads", false)
pause.isVisible = (queuedCount > 0 || activeCount > 0) && !pausedDownloads
resume.isVisible = (queuedCount > 0 || activeCount > 0) && pausedDownloads
} }
lifecycleScope.launch { resumeBtn.setOnClickListener {
downloadViewModel.activeQueuedDownloadsCount.collectLatest { lifecycleScope.launch {
if (it == 0) { downloadViewModel.resumeAllDownloads()
pause.isVisible = false
resume.isVisible = false
}
} }
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.activeDownloads.collectLatest { downloadViewModel.activePausedDownloads.collectLatest {
noResults.isVisible = it.isEmpty() noResults.isVisible = it.isEmpty()
activeDownloads.submitList(it) activeDownloads.submitList(it)
activeRecyclerView.scrollTo(0,0)
} }
} }
@ -235,32 +224,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item) downloadViewModel.updateDownload(item)
} }
val activeCount = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloadsCount()
}
if (activeCount == 0){
val queuedCount = withContext(Dispatchers.IO){
downloadViewModel.getQueuedDownloadsCount()
}
if (queuedCount == 0) {
preferences.edit().putBoolean("paused_downloads", false).apply()
}
}
if (activeCount == 1){
val queue = withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}
if (!preferences.getBoolean("paused_downloads", false)){
runBlocking {
downloadViewModel.queueDownloads(queue)
}
}
}
} }
} }
@ -275,4 +238,18 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
} }
} }
override fun onPauseClick(itemID: Long) {
lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
withContext(Dispatchers.IO){
downloadViewModel.updateToStatus(itemID, DownloadRepository.Status.Paused)
}
}
}
override fun onResumeClick(itemID: Long) {
downloadViewModel.resumeDownload(itemID)
}
} }

View file

@ -209,7 +209,7 @@ class DownloadQueueMainFragment : Fragment(){
when(m.itemId){ when(m.itemId){
R.id.clear_queue -> { R.id.clear_queue -> {
showDeleteDialog { showDeleteDialog {
cancelAllDownloads() downloadViewModel.cancelAllDownloads()
} }
} }
R.id.clear_cancelled -> { R.id.clear_cancelled -> {
@ -270,24 +270,6 @@ class DownloadQueueMainFragment : Fragment(){
deleteDialog.show() deleteDialog.show()
} }
@SuppressLint("NotifyDataSetChanged")
private fun cancelAllDownloads() {
sharedPreferences.edit().putBoolean("paused_downloads", false).apply()
fragmentAdapter.notifyDataSetChanged()
workManager.cancelAllWorkByTag("download")
lifecycleScope.launch {
val notificationUtil = NotificationUtil(requireContext())
val activeAndQueued = withContext(Dispatchers.IO){
downloadViewModel.getActiveAndQueuedDownloadIDs()
}
activeAndQueued.forEach { id ->
YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id.toInt())
}
downloadViewModel.cancelActiveQueued()
}
}
fun scrollToActive(){ fun scrollToActive(){
tabLayout.getTabAt(0)!!.select() tabLayout.getTabAt(0)!!.select()
viewPager2.setCurrentItem(0, true) viewPager2.setCurrentItem(0, true)

View file

@ -29,7 +29,9 @@ import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isVisible
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
@ -47,7 +49,9 @@ import com.deniscerri.ytdl.database.repository.HistoryRepository
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.ui.adapter.HistoryAdapter import com.deniscerri.ytdl.ui.adapter.HistoryAdapter
import com.deniscerri.ytdl.ui.downloadcard.FormatSelectionBottomSheetDialog.FormatCategory
import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NavbarUtil import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
@ -85,9 +89,9 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
private var uiHandler: Handler? = null private var uiHandler: Handler? = null
private var noResults: RelativeLayout? = null private var noResults: RelativeLayout? = null
private var selectionChips: LinearLayout? = null private var selectionChips: LinearLayout? = null
private var websiteGroup: ChipGroup? = null
private var historyList: List<HistoryItem?>? = null private var historyList: List<HistoryItem?>? = null
private var allhistoryList: List<HistoryItem?>? = null private var allhistoryList: List<HistoryItem?>? = null
private var websiteList: MutableList<String> = mutableListOf()
private lateinit var selectedObjects: ArrayList<HistoryItem> private lateinit var selectedObjects: ArrayList<HistoryItem>
private var actionMode : ActionMode? = null private var actionMode : ActionMode? = null
@ -112,7 +116,6 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
topAppBar = view.findViewById(R.id.history_toolbar) topAppBar = view.findViewById(R.id.history_toolbar)
noResults = view.findViewById(R.id.no_results) noResults = view.findViewById(R.id.no_results)
selectionChips = view.findViewById(R.id.history_selection_chips) selectionChips = view.findViewById(R.id.history_selection_chips)
websiteGroup = view.findViewById(R.id.website_chip_group)
uiHandler = Handler(Looper.getMainLooper()) uiHandler = Handler(Looper.getMainLooper())
sortChip = view.findViewById(R.id.sortChip) sortChip = view.findViewById(R.id.sortChip)
selectedObjects = arrayListOf() selectedObjects = arrayListOf()
@ -127,7 +130,6 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyList = mutableListOf() historyList = mutableListOf()
allhistoryList = mutableListOf()
historyAdapter = historyAdapter =
HistoryAdapter( HistoryAdapter(
@ -149,14 +151,14 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java] historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
historyViewModel.allItems.observe(viewLifecycleOwner) { historyViewModel.allItems.observe(viewLifecycleOwner) {
allhistoryList = it
if(it.isEmpty()){ if(it.isEmpty()){
noResults!!.visibility = VISIBLE noResults!!.visibility = VISIBLE
selectionChips!!.visibility = GONE selectionChips!!.visibility = GONE
websiteGroup!!.removeAllViews() topAppBar!!.menu.children.firstOrNull { m -> m.itemId == R.id.filters }?.isVisible = false
}else{ }else{
noResults!!.visibility = GONE noResults!!.visibility = GONE
selectionChips!!.visibility = VISIBLE selectionChips!!.visibility = VISIBLE
topAppBar!!.menu.children.firstOrNull { m -> m.itemId == R.id.filters }?.isVisible = true
updateWebsiteChips(it) updateWebsiteChips(it)
} }
} }
@ -306,6 +308,106 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
deleteDialog.show() deleteDialog.show()
} }
} }
R.id.filters -> {
val filterSheet = BottomSheetDialog(requireContext())
filterSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
filterSheet.setContentView(R.layout.history_other_filters_sheet)
//format status
val notDeleted = filterSheet.findViewById<TextView>(R.id.not_deleted)!!
val deleted = filterSheet.findViewById<TextView>(R.id.deleted)!!
when(historyViewModel.statusFilter.value) {
HistoryViewModel.HistoryStatus.ALL -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
}
HistoryViewModel.HistoryStatus.DELETED -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0)
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
}
HistoryViewModel.HistoryStatus.NOT_DELETED -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0)
}
else -> {}
}
notDeleted.setOnClickListener {
val status = historyViewModel.statusFilter.value
when (status) {
HistoryViewModel.HistoryStatus.ALL -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.DELETED)
}
HistoryViewModel.HistoryStatus.NOT_DELETED -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.UNSET)
}
HistoryViewModel.HistoryStatus.DELETED -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.ALL)
}
else -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.NOT_DELETED)
}
}
}
deleted.setOnClickListener {
val status = historyViewModel.statusFilter.value
when (status) {
HistoryViewModel.HistoryStatus.ALL -> {
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.NOT_DELETED)
}
HistoryViewModel.HistoryStatus.NOT_DELETED -> {
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.ALL)
}
HistoryViewModel.HistoryStatus.DELETED -> {
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.UNSET)
}
else -> {
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.DELETED)
}
}
}
if (websiteList.size < 2) {
filterSheet.findViewById<View>(R.id.websiteFilters)?.isVisible = false
}else{
val websiteGroup = filterSheet.findViewById<ChipGroup>(R.id.websitesChipGroup)
val websiteFilter = historyViewModel.websiteFilter.value
for (i in websiteList.indices) {
val w = websiteList[i]
val tmp = layoutinflater!!.inflate(R.layout.filter_chip, websiteGroup, false) as Chip
tmp.text = w
tmp.id = i
tmp.setOnClickListener {
Log.e(TAG, tmp.isChecked.toString())
if (tmp.isChecked) {
historyViewModel.setWebsiteFilter(tmp.text as String)
tmp.isChecked = true
} else {
historyViewModel.setWebsiteFilter("")
tmp.isChecked = false
}
}
if (w == websiteFilter){
tmp.isChecked = true
}
websiteGroup!!.addView(tmp)
}
}
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
filterSheet.behavior.peekHeight = displayMetrics.heightPixels
filterSheet.show()
}
} }
true true
} }
@ -374,80 +476,43 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
val audio = fragmentView!!.findViewById<Chip>(R.id.audio_chip) val audio = fragmentView!!.findViewById<Chip>(R.id.audio_chip)
audio.setOnClickListener { audio.setOnClickListener {
if (audio.isChecked) { if (audio.isChecked) {
historyViewModel.setFormatFilter("audio") historyViewModel.setTypeFilter(DownloadViewModel.Type.audio.toString())
audio.isChecked = true audio.isChecked = true
} else { } else {
historyViewModel.setFormatFilter("") historyViewModel.setTypeFilter("")
audio.isChecked = false audio.isChecked = false
} }
} }
val video = fragmentView!!.findViewById<Chip>(R.id.video_chip) val video = fragmentView!!.findViewById<Chip>(R.id.video_chip)
video.setOnClickListener { video.setOnClickListener {
if (video.isChecked) { if (video.isChecked) {
historyViewModel.setFormatFilter("video") historyViewModel.setTypeFilter(DownloadViewModel.Type.video.toString())
video.isChecked = true video.isChecked = true
} else { } else {
historyViewModel.setFormatFilter("") historyViewModel.setTypeFilter("")
video.isChecked = false video.isChecked = false
} }
} }
val command = fragmentView!!.findViewById<Chip>(R.id.command_chip) val command = fragmentView!!.findViewById<Chip>(R.id.command_chip)
command.setOnClickListener { command.setOnClickListener {
if (command.isChecked) { if (command.isChecked) {
historyViewModel.setFormatFilter("command") historyViewModel.setTypeFilter(DownloadViewModel.Type.command.toString())
command.isChecked = true command.isChecked = true
} else { } else {
historyViewModel.setFormatFilter("") historyViewModel.setTypeFilter("")
command.isChecked = false command.isChecked = false
} }
} }
val notDeleted = fragmentView!!.findViewById<Chip>(R.id.not_deleted_chip)
notDeleted.setOnClickListener {
if (notDeleted.isChecked) {
historyViewModel.setNotDeleted(true)
notDeleted.isChecked = true
} else {
historyViewModel.setNotDeleted(false)
notDeleted.isChecked = false
}
}
} }
private fun updateWebsiteChips(list : List<HistoryItem>) { private fun updateWebsiteChips(list : List<HistoryItem>) {
val websites = mutableListOf<String>() websiteList = mutableListOf()
val websiteFilter = historyViewModel.websiteFilter.value
for (item in list){ for (item in list){
if (!websites.contains(item.website)) websites.add(item.website) if (item.website == "null" || item.website.isEmpty()) continue
if (!websiteList.contains(item.website)) websiteList.add(item.website)
} }
websiteGroup!!.removeAllViews()
if (websites.size <= 1) {
requireView().findViewById<View>(R.id.website_divider).visibility = GONE
return
}
//val websites = historyRecyclerViewAdapter!!.websites
for (i in websites.indices) {
val w = websites[i]
if (w == "null" || w.isEmpty()) continue
val tmp = layoutinflater!!.inflate(R.layout.filter_chip, websiteGroup, false) as Chip
tmp.text = w
tmp.id = i
tmp.setOnClickListener {
Log.e(TAG, tmp.isChecked.toString())
if (tmp.isChecked) {
historyViewModel.setWebsiteFilter(tmp.text as String)
tmp.isChecked = true
} else {
historyViewModel.setWebsiteFilter("")
tmp.isChecked = false
}
}
if (w == websiteFilter){
tmp.isChecked = true
}
websiteGroup!!.addView(tmp)
}
requireView().findViewById<View>(R.id.website_divider).visibility = VISIBLE
} }
override fun onCardClick(itemID: Long, isPresent: Boolean) { override fun onCardClick(itemID: Long, isPresent: Boolean) {

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
@ -23,6 +24,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
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
@ -43,6 +45,7 @@ import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
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
@ -64,6 +67,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
private lateinit var mainActivity: MainActivity private lateinit var mainActivity: MainActivity
private lateinit var sortChip: Chip private lateinit var sortChip: Chip
private lateinit var selectedObjects: ArrayList<CommandTemplate> private lateinit var selectedObjects: ArrayList<CommandTemplate>
private lateinit var preferences: SharedPreferences
private var actionMode : ActionMode? = null private var actionMode : ActionMode? = null
private val jsonFormat = Json { prettyPrint = true } private val jsonFormat = Json { prettyPrint = true }
@ -95,7 +99,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
recyclerView = view.findViewById(R.id.template_recyclerview) recyclerView = view.findViewById(R.id.template_recyclerview)
recyclerView.layoutManager = LinearLayoutManager(context) recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = templatesAdapter recyclerView.adapter = templatesAdapter
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("templates")){ if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("templates")){
val itemTouchHelper = ItemTouchHelper(simpleCallback) val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recyclerView) itemTouchHelper.attachToRecyclerView(recyclerView)
@ -117,8 +121,8 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
commandTemplateViewModel.sortOrder.observe(viewLifecycleOwner){ commandTemplateViewModel.sortOrder.observe(viewLifecycleOwner){
if (it != null){ if (it != null){
when(it){ when(it){
SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down) SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up)
SORTING.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up) SORTING.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down)
} }
} }
} }
@ -264,9 +268,10 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
} }
} }
@SuppressLint("NotifyDataSetChanged")
override fun onItemClick(commandTemplate: CommandTemplate, index: Int) { override fun onItemClick(commandTemplate: CommandTemplate, index: Int) {
UiUtil.showCommandTemplateCreationOrUpdatingSheet(commandTemplate,mainActivity, this, commandTemplateViewModel, newTemplate = { UiUtil.showCommandTemplateCreationOrUpdatingSheet(commandTemplate,mainActivity, this, commandTemplateViewModel, newTemplate = {
templatesAdapter.notifyItemChanged(index) templatesAdapter.notifyDataSetChanged()
}, {}) }, {})
} }

View file

@ -145,14 +145,13 @@ class MoreFragment : Fragment() {
} }
activeDownloadsList.forEach { activeDownloadsList.forEach {
it.status = DownloadRepository.Status.Queued.toString()
YoutubeDL.getInstance().destroyProcessById(it.id.toString()) YoutubeDL.getInstance().destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt()) notificationUtil.cancelDownloadNotification(it.id.toInt())
it.status = DownloadRepository.Status.Paused.toString()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
downloadViewModel.updateDownload(it) downloadViewModel.updateDownload(it)
} }
} }
mainSharedPreferencesEditor.putBoolean("paused_downloads", true).apply()
} }
if (doNotShowAgain){ if (doNotShowAgain){

View file

@ -148,6 +148,14 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
backup = findPreference("backup") backup = findPreference("backup")
restore = findPreference("restore") restore = findPreference("restore")
findPreference<Preference>("package_name")?.apply {
summary = BuildConfig.APPLICATION_ID
setOnPreferenceClickListener {
UiUtil.copyToClipboard(summary.toString(), requireActivity())
true
}
}
backup!!.onPreferenceClickListener = backup!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
val builder = MaterialAlertDialogBuilder(requireContext()) val builder = MaterialAlertDialogBuilder(requireContext())

View file

@ -20,6 +20,7 @@ import android.net.Uri
import android.text.Html import android.text.Html
import android.text.Spanned import android.text.Spanned
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Patterns
import android.util.TypedValue import android.util.TypedValue
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@ -45,16 +46,22 @@ import com.deniscerri.ytdl.database.models.Format
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.isYoutubeChannelURL
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.toTimePeriodsArray 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
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.neo.highlight.core.Highlight import com.neo.highlight.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme import com.neo.highlight.util.scheme.ColorScheme
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import me.zhanghai.android.fastscroll.FastScrollerBuilder import me.zhanghai.android.fastscroll.FastScrollerBuilder
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
@ -212,7 +219,13 @@ object Extensions {
tags.forEach { tags.forEach {
runCatching { runCatching {
val tmp = this.getString(it) val tmp = this.getString(it)
if (tmp != "null") return tmp if (tmp != "null") {
return if (tmp.startsWith("[")) {
Json.decodeFromString<List<String>>(tmp).joinToString(", ")
}else{
tmp
}
}
} }
} }
@ -508,4 +521,16 @@ object Extensions {
return Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?(be.com))/@[a-zA-Z]+").matcher(this).find() return Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?(be.com))/@[a-zA-Z]+").matcher(this).find()
} }
fun String.isYoutubeWatchVideosURL() : Boolean {
return Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?(be.com))/watch_videos\\?video_ids=.*").matcher(this).find()
}
fun String.extractURL() : String {
val res = Pattern.compile("(http|ftp|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])").matcher(this)
return if (res.find()){
res.group()
}else{
this
}
}
} }

View file

@ -8,6 +8,7 @@ import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.Format
import java.util.Collections
class FormatUtil(private var context: Context) { class FormatUtil(private var context: Context) {
private val sharedPreferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) private val sharedPreferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@ -19,8 +20,13 @@ class FormatUtil(private var context: Context) {
private val videoCodecPreference : String = sharedPreferences.getString("video_codec", "").toString() private val videoCodecPreference : String = sharedPreferences.getString("video_codec", "").toString()
private val audioContainerPreference : String = sharedPreferences.getString("audio_format", "").toString() private val audioContainerPreference : String = sharedPreferences.getString("audio_format", "").toString()
private val videoContainerPreference : String = sharedPreferences.getString("video_format", "").toString() private val videoContainerPreference : String = sharedPreferences.getString("video_format", "").toString()
@SuppressLint("RestrictedApi") private val videoResolutionOrder = context.getStringArray(R.array.video_formats_values)
private val videoResolutionOrder = context.getStringArray(R.array.video_formats_values).filter { it.contains("_") }.map{ it.split("_")[0].dropLast(1) }.reversed() .filter { it.contains("_") }
.map{ it.split("_")[0].dropLast(1) }.toMutableList().apply {
this.reverse()
}
private val preferSmallerFormats = sharedPreferences.getBoolean("prefer_smaller_formats", false) private val preferSmallerFormats = sharedPreferences.getBoolean("prefer_smaller_formats", false)
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")

View file

@ -182,9 +182,12 @@ object UiUtil {
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.create_command_template) bottomSheet.setContentView(R.layout.create_command_template)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val ok : Button = bottomSheet.findViewById(R.id.template_create)!! val ok : Button = bottomSheet.findViewById(R.id.template_create)!!
val title : TextInputLayout = bottomSheet.findViewById(R.id.title)!! val title : TextInputLayout = bottomSheet.findViewById(R.id.title)!!
val content : TextInputLayout = bottomSheet.findViewById(R.id.content)!! val content : TextInputLayout = bottomSheet.findViewById(R.id.content)!!
val preferredCommandSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.preferredCommandTemplateSwitch)!!
val extraCommandsSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.extraCommandsSwitch)!! val extraCommandsSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.extraCommandsSwitch)!!
val extraCommandsAudio : CheckBox = bottomSheet.findViewById(R.id.checkbox_audio)!! val extraCommandsAudio : CheckBox = bottomSheet.findViewById(R.id.checkbox_audio)!!
val extraCommandsVideo : CheckBox = bottomSheet.findViewById(R.id.checkbox_video)!! val extraCommandsVideo : CheckBox = bottomSheet.findViewById(R.id.checkbox_video)!!
@ -248,6 +251,8 @@ object UiUtil {
} }
if (item != null){ if (item != null){
preferredCommandSwitch.isChecked = item.content == sharedPreferences.getString("preferred_command_template", "")
extraCommandsSwitch.isChecked = item.useAsExtraCommand extraCommandsSwitch.isChecked = item.useAsExtraCommand
if (item.useAsExtraCommand){ if (item.useAsExtraCommand){
extraCommandsAudio.isVisible = true extraCommandsAudio.isVisible = true
@ -294,6 +299,13 @@ object UiUtil {
} }
ok.setOnClickListener { ok.setOnClickListener {
if (preferredCommandSwitch.isChecked) {
sharedPreferences.edit().putString("preferred_command_template", content.editText!!.text.toString()).apply()
}else if (sharedPreferences.getString("preferred_command_template", "") == item?.content){
sharedPreferences.edit().putString("preferred_command_template", "").apply()
}
if (item == null){ if (item == null){
val t = CommandTemplate(0, title.editText!!.text.toString(), content.editText!!.text.toString(), extraCommandsSwitch.isChecked, extraCommandsAudio.isChecked, extraCommandsVideo.isChecked) val t = CommandTemplate(0, title.editText!!.text.toString(), content.editText!!.text.toString(), extraCommandsSwitch.isChecked, extraCommandsAudio.isChecked, extraCommandsVideo.isChecked)
commandTemplateViewModel.insert(t) commandTemplateViewModel.insert(t)
@ -304,7 +316,6 @@ object UiUtil {
item.useAsExtraCommand = extraCommandsSwitch.isChecked item.useAsExtraCommand = extraCommandsSwitch.isChecked
item.useAsExtraCommandAudio = extraCommandsAudio.isChecked item.useAsExtraCommandAudio = extraCommandsAudio.isChecked
item.useAsExtraCommandVideo = extraCommandsVideo.isChecked item.useAsExtraCommandVideo = extraCommandsVideo.isChecked
Log.e("aa", item.toString())
commandTemplateViewModel.update(item) commandTemplateViewModel.update(item)
newTemplate(item) newTemplate(item)
} }
@ -1037,7 +1048,7 @@ object UiUtil {
} }
suspend fun showCommandTemplates(activity: Activity, commandTemplateViewModel: CommandTemplateViewModel, itemSelected: (itemSelected: List<CommandTemplate>) -> Unit) { suspend fun showCommandTemplates(activity: Activity, commandTemplateViewModel: CommandTemplateViewModel, onlyOne: Boolean = false, itemSelected: (itemSelected: List<CommandTemplate>) -> Unit) {
val bottomSheet = BottomSheetDialog(activity) val bottomSheet = BottomSheetDialog(activity)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.command_template_list) bottomSheet.setContentView(R.layout.command_template_list)
@ -1050,6 +1061,7 @@ object UiUtil {
linearLayout!!.removeAllViews() linearLayout!!.removeAllViews()
val selectedItems = mutableListOf<CommandTemplate>() val selectedItems = mutableListOf<CommandTemplate>()
val ok = bottomSheet.findViewById<MaterialButton>(R.id.command_ok) val ok = bottomSheet.findViewById<MaterialButton>(R.id.command_ok)
ok?.isVisible = !onlyOne
ok?.isEnabled = list.size == 1 ok?.isEnabled = list.size == 1
list.forEach {template -> list.forEach {template ->
@ -1065,6 +1077,11 @@ object UiUtil {
(it as MaterialCardView).isChecked = true (it as MaterialCardView).isChecked = true
} }
if (onlyOne) {
itemSelected(listOf(template))
bottomSheet.cancel()
}
ok?.isEnabled = selectedItems.isNotEmpty() ok?.isEnabled = selectedItems.isNotEmpty()
} }
linearLayout.addView(item) linearLayout.addView(item)

View file

@ -21,6 +21,7 @@ import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
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.isYoutubeURL import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.isYoutubeWatchVideosURL
import com.deniscerri.ytdl.util.Extensions.toStringDuration import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatUtil import com.deniscerri.ytdl.util.FormatUtil
@ -51,7 +52,14 @@ class YTDLPUtil(private val context: Context) {
val request : YoutubeDLRequest val request : YoutubeDLRequest
if (query.contains("http")){ if (query.contains("http")){
request = YoutubeDLRequest(query) if (query.isYoutubeWatchVideosURL()) {
request = YoutubeDLRequest(emptyList())
val config = File(context.cacheDir.absolutePath + "/config" + System.currentTimeMillis() + "##url.txt")
config.writeText(query)
request.addOption("--config", config.absolutePath)
}else{
request = YoutubeDLRequest(query)
}
}else{ }else{
request = YoutubeDLRequest(emptyList()) request = YoutubeDLRequest(emptyList())
when (searchEngine){ when (searchEngine){
@ -64,13 +72,8 @@ class YTDLPUtil(private val context: Context) {
} }
} }
} }
val lang = sharedPreferences.getString("app_language", "en")
if (searchEngine == "ytsearch" || query.isYoutubeURL()) { if (searchEngine == "ytsearch" || query.isYoutubeURL()) {
var extractorArgs = "player_client=default,mediaconnect" request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
if (context.getStringArray(R.array.subtitle_langs).contains(lang)) {
extractorArgs += ";lang=$lang"
}
request.addOption("--extractor-args", "youtube:$extractorArgs")
} }
request.addOption("--flat-playlist") request.addOption("--flat-playlist")
@ -103,7 +106,7 @@ class YTDLPUtil(private val context: Context) {
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp") request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
println(parseYTDLRequestString(request))
val youtubeDLResponse = YoutubeDL.getInstance().execute(request) val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val results: List<String?> = try { val results: List<String?> = try {
val lineSeparator = System.getProperty("line.separator") val lineSeparator = System.getProperty("line.separator")
@ -119,7 +122,7 @@ class YTDLPUtil(private val context: Context) {
val title = jsonObject.getStringByAny("alt_title", "title", "webpage_url_basename") val title = jsonObject.getStringByAny("alt_title", "title", "webpage_url_basename")
if (title == "[Private video]" || title == "[Deleted video]") continue if (title == "[Private video]" || title == "[Deleted video]") continue
var author = jsonObject.getStringByAny("artist", "uploader", "channel", "playlist_uploader", "uploader_id") var author = jsonObject.getStringByAny("artists", "artist", "uploader", "channel", "playlist_uploader", "uploader_id")
var duration = jsonObject.getIntByAny("duration").toString() var duration = jsonObject.getIntByAny("duration").toString()
if (duration != "-1"){ if (duration != "-1"){
duration = jsonObject.getInt("duration").toStringDuration(Locale.US) duration = jsonObject.getInt("duration").toStringDuration(Locale.US)
@ -689,12 +692,7 @@ class YTDLPUtil(private val context: Context) {
} }
if (downloadItem.url.isYoutubeURL()) { if (downloadItem.url.isYoutubeURL()) {
var extractorArgs = "player_client=default,mediaconnect" request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
val lang = sharedPreferences.getString("app_language", "en")
if (context.getStringArray(R.array.subtitle_langs).contains(lang)) {
extractorArgs += ";lang=$lang"
}
request.addOption("--extractor-args", "youtube:$extractorArgs")
} }
} }
@ -1098,4 +1096,18 @@ class YTDLPUtil(private val context: Context) {
} }
return request return request
} }
private fun getYoutubeExtractorArgs() : String {
var extractorArgs = "player_client=default,mediaconnect"
val lang = sharedPreferences.getString("app_language", "en")
if (context.getStringArray(R.array.subtitle_langs).contains(lang)) {
extractorArgs += ";lang=$lang"
}
val poToken = sharedPreferences.getString("youtube_po_token", "")!!
if (poToken.isNotBlank()) {
extractorArgs += ";po_token=web+$poToken"
}
return extractorArgs
}
} }

View file

@ -151,6 +151,8 @@ class NewPipeUtil(context: Context) {
fun getChannelData(url: String, progress: (pagedResults: MutableList<ResultItem>) -> Unit) : Result<List<ResultItem>> { fun getChannelData(url: String, progress: (pagedResults: MutableList<ResultItem>) -> Unit) : Result<List<ResultItem>> {
try { try {
//TODO BROKEN FOR NOW
return Result.failure(Throwable())
val req = ChannelInfo.getInfo(ServiceList.YouTube, url) val req = ChannelInfo.getInfo(ServiceList.YouTube, url)
println(Gson().toJson(req)) println(Gson().toJson(req))
val items = mutableListOf<ResultItem>() val items = mutableListOf<ResultItem>()
@ -326,7 +328,7 @@ class NewPipeUtil(context: Context) {
if (stream.audioStreams.isNotEmpty()){ if (stream.audioStreams.isNotEmpty()){
for (f in 0 until stream.audioStreams.size){ for (f in 0 until stream.audioStreams.size){
val it = stream.audioStreams[f] val it = stream.audioStreams[f]
if (it.bitrate == 0) continue if (it.bitrate == 0 || listOf(599, 600).contains(it.itag)) continue
val formatObj = Format( val formatObj = Format(
format_id = it.itag.toString(), format_id = it.itag.toString(),

View file

@ -18,7 +18,6 @@ import androidx.work.ForegroundInfo
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.await
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.App import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.MainActivity
@ -38,6 +37,7 @@ import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -53,6 +53,13 @@ class DownloadWorker(
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
if (isStopped) return Result.success() if (isStopped) return Result.success()
val workManager = WorkManager.getInstance(context)
val currentWork = withContext(Dispatchers.IO) {
workManager.getWorkInfosByTag("download").get()
}
if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) {
return Result.success()
}
val notificationUtil = NotificationUtil(App.instance) val notificationUtil = NotificationUtil(App.instance)
val ytdlpUtil = YTDLPUtil(context) val ytdlpUtil = YTDLPUtil(context)
@ -66,10 +73,11 @@ class DownloadWorker(
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val time = System.currentTimeMillis() + 6000 val time = System.currentTimeMillis() + 6000
val queuedItems = dao.getQueuedScheduledDownloadsUntil(time) val queuedItems = dao.getQueuedScheduledDownloadsUntil(time)
val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await() val priorityItemIDs = inputData.getLongArray("priority_item_ids")!!.toMutableList()
if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success() val continueAfterPriorityIds = inputData.getBoolean("continue_after_priority_ids", true)
// this is needed for observe sources call, so it wont create result items // this is needed for observe sources call, so it wont create result items
// [removed]
//val createResultItem = inputData.getBoolean("createResultItem", true) //val createResultItem = inputData.getBoolean("createResultItem", true)
val confTmp = Configuration(context.resources.configuration) val confTmp = Configuration(context.resources.configuration)
@ -100,8 +108,9 @@ class DownloadWorker(
val foregroundInfo = ForegroundInfo(1000000000, workNotif) val foregroundInfo = ForegroundInfo(1000000000, workNotif)
setForegroundAsync(foregroundInfo) setForegroundAsync(foregroundInfo)
queuedItems.collect { items -> queuedItems.collectLatest { items ->
if (isStopped) return@collect if (this@DownloadWorker.isStopped) return@collectLatest
runningYTDLInstances.clear() runningYTDLInstances.clear()
val activeDownloads = dao.getActiveDownloadsList() val activeDownloads = dao.getActiveDownloadsList()
activeDownloads.forEach { activeDownloads.forEach {
@ -112,17 +121,28 @@ class DownloadWorker(
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (items.isEmpty() && running.isEmpty()) { if (items.isEmpty() && running.isEmpty()) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
return@collect return@collectLatest
} }
if (useScheduler){ if (useScheduler){
if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) { if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
return@collect return@collectLatest
} }
} }
if (priorityItemIDs.isEmpty() && !continueAfterPriorityIds) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
return@collectLatest
}
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size
val eligibleDownloads = items.take(if (concurrentDownloads < 0) 0 else concurrentDownloads).filter { it.id !in running } val eligibleDownloads = if (priorityItemIDs.isNotEmpty()) {
val tmp = priorityItemIDs.take(concurrentDownloads)
items.filter { it.id !in running && tmp.contains(it.id) }
}else{
items.take(concurrentDownloads).filter { it.id !in running }
}
eligibleDownloads.forEach{downloadItem -> eligibleDownloads.forEach{downloadItem ->
val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url }) val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url })
@ -365,7 +385,10 @@ class DownloadWorker(
} }
if (eligibleDownloads.isNotEmpty()){ if (eligibleDownloads.isNotEmpty()){
eligibleDownloads.forEach { it.status = DownloadRepository.Status.Active.toString() } eligibleDownloads.forEach {
it.status = DownloadRepository.Status.Active.toString()
priorityItemIDs.remove(it.id)
}
dao.updateMultiple(eligibleDownloads) dao.updateMultiple(eligibleDownloads)
} }
} }

View file

@ -180,7 +180,7 @@ class ObserveSourceWorker(
} }
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || queuedItems.any { it.downloadStartTime > 0L } ){ if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || queuedItems.any { it.downloadStartTime > 0L } ){
downloadRepo.startDownloadWorker(queuedItems, context, Data.Builder().putBoolean("createResultItem", false)) downloadRepo.startDownloadWorker(queuedItems, context)
if(!useScheduler){ if(!useScheduler){
queuedItems.filter { it.downloadStartTime != 0L || (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach { queuedItems.filter { it.downloadStartTime != 0L || (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="?attr/colorAccent" android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View file

@ -128,13 +128,14 @@
<com.google.android.material.search.SearchBar <com.google.android.material.search.SearchBar
android:id="@+id/search_bar" android:id="@+id/search_bar"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:hint="@string/search_hint" android:hint="@string/search_hint"
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="1.0"
android:layout_marginStart="60dp"
app:layout_constraintStart_toEndOf="@+id/home_toolbar" app:layout_constraintStart_toEndOf="@+id/home_toolbar"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/main_menu"> app:menu="@menu/main_menu">

View file

@ -166,21 +166,21 @@
app:layout_constraintVertical_bias="0.51" app:layout_constraintVertical_bias="0.51"
> >
<!-- <com.google.android.material.button.MaterialButton--> <com.google.android.material.button.MaterialButton
<!-- android:id="@+id/active_download_resume"--> android:id="@+id/active_download_resume"
<!-- style="?attr/materialIconButtonFilledStyle"--> style="?attr/materialIconButtonFilledStyle"
<!-- android:layout_width="wrap_content"--> android:layout_width="wrap_content"
<!-- android:contentDescription="@string/resume"--> android:contentDescription="@string/resume"
<!-- android:layout_height="wrap_content"--> android:layout_height="wrap_content"
<!-- app:backgroundTint="?attr/colorSurface"--> app:backgroundTint="?attr/colorSurface"
<!-- app:cornerRadius="15dp"--> app:cornerRadius="15dp"
<!-- app:icon="@drawable/exomedia_ic_play_arrow_white"--> app:icon="@drawable/exomedia_ic_play_arrow_white"
<!-- app:iconSize="30dp"--> app:iconSize="30dp"
<!-- app:iconTint="?android:textColorPrimary"--> app:iconTint="?android:textColorPrimary"
<!-- app:layout_constraintBottom_toTopOf="@+id/output"--> app:layout_constraintBottom_toTopOf="@+id/output"
<!-- app:layout_constraintEnd_toEndOf="parent"--> app:layout_constraintEnd_toEndOf="parent"
<!-- app:layout_constraintTop_toBottomOf="@+id/linearlayout2_horizontalscrollview"--> app:layout_constraintTop_toBottomOf="@+id/linearlayout2_horizontalscrollview"
<!-- app:layout_constraintVertical_bias="0.51" />--> app:layout_constraintVertical_bias="0.51" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/active_download_delete" android:id="@+id/active_download_delete"

View file

@ -41,26 +41,66 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" /> app:layout_constraintTop_toBottomOf="@+id/title" />
<TextView <HorizontalScrollView
android:id="@+id/useInExtraCommands"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:scrollbars="none"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:visibility="gone"
android:ellipsize="end"
android:text="@string/extra_command"
android:gravity="center"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/content" /> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/content"
app:layout_constraintHorizontal_bias="0.0"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/preferredTemplate"
style="@style/Widget.Material3.FloatingActionButton.Large.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorPrimaryContainer"
android:clickable="false"
android:gravity="center"
android:visibility="gone"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
android:text="@string/preferred_command_template"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/useInExtraCommands"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginEnd="5dp"
android:text="@string/extra_command"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondaryContainer"
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>
</HorizontalScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -102,54 +102,60 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/preferredCommandTemplateSwitch"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="10dp"
android:checked="false"
android:text="@string/preferred_command_template"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.card.MaterialCardView <View
style="@style/Divider.Horizontal"
android:layout_marginVertical="10dp" />
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:cardCornerRadius="20dp"> android:layout_marginHorizontal="10dp"
android:orientation="vertical">
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/extraCommandsSwitch"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:checked="false"
android:text="@string/extra_command"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp" android:orientation="horizontal">
android:orientation="vertical">
<com.google.android.material.materialswitch.MaterialSwitch <CheckBox
android:id="@+id/extraCommandsSwitch" android:id="@+id/checkbox_audio"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="match_parent"
android:checked="false"
android:text="@string/extra_command"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:layout_marginEnd="20dp"
android:visibility="gone"
android:checked="true"
android:text="@string/audio" />
<CheckBox <CheckBox
android:id="@+id/checkbox_audio" android:id="@+id/checkbox_video"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="20dp" android:checked="true"
android:visibility="gone" android:visibility="gone"
android:checked="true" android:text="@string/video" />
android:text="@string/audio" />
<CheckBox
android:id="@+id/checkbox_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:visibility="gone"
android:text="@string/video" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </LinearLayout>
<View <View
style="@style/Divider.Horizontal" style="@style/Divider.Horizontal"

View file

@ -19,8 +19,8 @@
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:orientation="horizontal" android:layout_marginTop="20dp"
android:paddingTop="20dp"> android:orientation="horizontal">
<com.facebook.shimmer.ShimmerFrameLayout <com.facebook.shimmer.ShimmerFrameLayout
android:visibility="gone" android:visibility="gone"

View file

@ -17,9 +17,9 @@
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/pause" android:text="@string/pause_all"
android:layout_margin="16dp" android:layout_margin="16dp"
android:contentDescription="@string/pause" android:contentDescription="@string/pause_all"
app:icon="@drawable/exomedia_ic_pause_white" app:icon="@drawable/exomedia_ic_pause_white"
app:useCompatPadding="true" /> app:useCompatPadding="true" />
@ -29,9 +29,9 @@
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/resume" android:text="@string/resume_all"
android:layout_margin="16dp" android:layout_margin="16dp"
android:contentDescription="@string/resume" android:contentDescription="@string/resume_all"
app:icon="@drawable/exomedia_ic_play_arrow_white" app:icon="@drawable/exomedia_ic_play_arrow_white"
app:useCompatPadding="true" /> app:useCompatPadding="true" />

View file

@ -94,7 +94,6 @@
</androidx.recyclerview.widget.RecyclerView> </androidx.recyclerview.widget.RecyclerView>
<include layout="@layout/no_results" <include layout="@layout/no_results"
android:visibility="gone" /> android:visibility="gone" />

View file

@ -37,7 +37,6 @@
<LinearLayout <LinearLayout
android:id="@+id/history_selection_chips" android:id="@+id/history_selection_chips"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:visibility="gone"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup <com.google.android.material.chip.ChipGroup
@ -58,16 +57,6 @@
<View style="@style/Divider.Vertical"/> <View style="@style/Divider.Vertical"/>
<com.google.android.material.chip.Chip
android:id="@+id/not_deleted_chip"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/not_deleted"/>
<View android:id="@+id/notdeleted_divider" style="@style/Divider.Vertical"/>
<com.google.android.material.chip.ChipGroup <com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content" android:layout_width="wrap_content"
app:selectionRequired="false" app:selectionRequired="false"
@ -97,17 +86,6 @@
</com.google.android.material.chip.ChipGroup> </com.google.android.material.chip.ChipGroup>
<View android:id="@+id/website_divider" style="@style/Divider.Vertical"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/website_chip_group"
app:selectionRequired="false"
app:singleSelection="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</com.google.android.material.chip.ChipGroup>
</LinearLayout> </LinearLayout>
</HorizontalScrollView> </HorizontalScrollView>

View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="@+id/format_filter_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:text="@string/status"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="10dp"
android:layout_marginTop="20dp"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/not_deleted"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:drawablePadding="30dp"
android:focusable="true"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:text="@string/not_deleted"
android:textSize="15sp"
app:drawableStartCompat="@drawable/empty" />
<TextView
android:id="@+id/deleted"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:drawablePadding="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:text="@string/deleted"
app:drawableStartCompat="@drawable/empty" />
</LinearLayout>
<LinearLayout
android:id="@+id/websiteFilters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="10dp"
android:layout_marginTop="20dp"
android:text="@string/websites"
android:textSize="14sp"
android:textStyle="bold" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/websitesChipGroup"
android:layout_width="wrap_content"
app:chipSpacingVertical="-10dp"
app:chipSpacing="0dp"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="20dp"
app:singleSelection="true"
android:layout_height="wrap_content"
app:singleLine="false">
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</LinearLayout>

View file

@ -40,6 +40,7 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxLength="9"
android:inputType="number" /> android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@ -57,6 +58,7 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxLength="9"
android:inputType="text" /> android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>

View file

@ -3,19 +3,19 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity" > tools:context=".MainActivity" >
<!-- <group>--> <group>
<!-- <item--> <item
<!-- android:id="@+id/pause"--> android:id="@+id/pause"
<!-- android:title="@string/pause"--> android:title="@string/pause"
<!-- android:icon="@drawable/exomedia_ic_pause_white"--> android:icon="@drawable/exomedia_ic_pause_white"
<!-- app:showAsAction="ifRoom" />--> app:showAsAction="ifRoom" />
<!-- <item--> <item
<!-- android:id="@+id/resume"--> android:id="@+id/resume"
<!-- android:title="@string/resume"--> android:title="@string/resume"
<!-- android:icon="@drawable/exomedia_ic_play_arrow_white"--> android:icon="@drawable/exomedia_ic_play_arrow_white"
<!-- app:showAsAction="ifRoom" />--> app:showAsAction="ifRoom" />
<!-- </group>--> </group>
<item <item
android:id="@+id/cancel" android:id="@+id/cancel"

View file

@ -10,6 +10,12 @@
app:showAsAction="collapseActionView|always" app:showAsAction="collapseActionView|always"
app:actionViewClass="androidx.appcompat.widget.SearchView"/> app:actionViewClass="androidx.appcompat.widget.SearchView"/>
<item
android:id="@+id/filters"
android:title="Filters"
android:icon="@drawable/baseline_filter_alt_24"
app:showAsAction="always"/>
<item <item
android:id="@+id/download_queue" android:id="@+id/download_queue"
android:title="@string/download_queue" android:title="@string/download_queue"

View file

@ -411,7 +411,7 @@
<string name="use_alarm_manager">Use AlarmManager instead of WorkManager for Scheduling</string> <string name="use_alarm_manager">Use AlarmManager instead of WorkManager for Scheduling</string>
<string name="use_alarm_manager_summary">Enable this if WorkManager is restricted by your device vendor or scheduled jobs are not too precise</string> <string name="use_alarm_manager_summary">Enable this if WorkManager is restricted by your device vendor or scheduled jobs are not too precise</string>
<string name="use_item_url_not_playlist">Use Item URL instead of Playlist URL</string> <string name="use_item_url_not_playlist">Use Item URL instead of Playlist URL</string>
<string name="use_item_url_not_playlist_summary">Useful for Libretube offline playlists that are not recognised by yt-dlp. Playlist metadata wont be embedded with this enabled</string> <string name="use_item_url_not_playlist_summary">Playlist metadata wont be embedded with this enabled</string>
<string name="recode_video">Recode Video</string> <string name="recode_video">Recode Video</string>
<string name="recode_video_summary">Recodes the video file to the specified video format</string> <string name="recode_video_summary">Recodes the video file to the specified video format</string>
<string name="daily">Daily</string> <string name="daily">Daily</string>
@ -422,4 +422,11 @@
<string name="playlist_as_album">Use Playlist Name as Album Metadata</string> <string name="playlist_as_album">Use Playlist Name as Album Metadata</string>
<string name="playlist_as_album_summary">If Album Metadata is not present, use playlist name instead</string> <string name="playlist_as_album_summary">If Album Metadata is not present, use playlist name instead</string>
<string name="data_fetching_extractor_youtube">Data Fetching Extractor (YouTube)</string> <string name="data_fetching_extractor_youtube">Data Fetching Extractor (YouTube)</string>
<string name="pause_all">Pause All</string>
<string name="resume_all">Resume All</string>
<string name="package_name">Package Name</string>
<string name="preferred_command_template">Preferred Command Template</string>
<string name="status">Status</string>
<string name="deleted">Deleted</string>
<string name="websites">Websites</string>
</resources> </resources>

View file

@ -102,6 +102,13 @@
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:title="@string/format_source" /> app:title="@string/format_source" />
<EditTextPreference
android:defaultValue=""
android:icon="@drawable/ic_code"
android:key="youtube_po_token"
app:useSimpleSummaryProvider="true"
android:title="PO Token" />
<Preference <Preference
android:icon="@drawable/ic_dns" android:icon="@drawable/ic_dns"
app:key="piped_instance" app:key="piped_instance"

View file

@ -39,6 +39,10 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/about" > <PreferenceCategory android:title="@string/about" >
<Preference
app:icon="@drawable/baseline_exit_to_app_24"
app:key="package_name"
app:title="@string/package_name" />
<Preference <Preference
app:icon="@drawable/ic_discord" app:icon="@drawable/ic_discord"
app:key="discord" app:key="discord"

View file

@ -15,7 +15,8 @@ buildscript {
// supports java 1.6 // supports java 1.6
commonsCompressVer = '1.12' commonsCompressVer = '1.12'
youtubedlAndroidVer = "ed652962e5" youtubedlAndroidVer = "ed652962e5"
youtubedlAndroidVerNoPyCrypto = "97bee6c0e8" youtubedlAndroidVerNoPyCrypto = "97bee6c0e8"
youtubedlAndroidVerMine = "8831745ea3"
oldytdlAndroidVer = "0.14.0" oldytdlAndroidVer = "0.14.0"
workVer = "2.9.1" workVer = "2.9.1"
composeVer = '1.4.2' composeVer = '1.4.2'
@ -23,8 +24,8 @@ buildscript {
coroutineVer = '1.7.3' coroutineVer = '1.7.3'
retrofitVer = "2.9.0" retrofitVer = "2.9.0"
kodeinVer = "7.16.0" kodeinVer = "7.16.0"
navVer = "2.7.7" navVer = "2.8.3"
media3_version = "1.4.0" media3_version = "1.4.1"
agp_version = '8.1.0' agp_version = '8.1.0'
agp_version1 = '7.4.2' agp_version1 = '7.4.2'
roomVer = '2.5.2' roomVer = '2.5.2'
@ -38,16 +39,15 @@ buildscript {
} }
plugins { plugins {
id 'com.android.application' version '8.4.0' apply false id 'com.android.application' version '8.5.2' apply false
id 'com.android.library' version '8.4.0' apply false id 'com.android.library' version '8.5.2' 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.4.0' apply false id 'com.android.test' version '8.5.2' 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
} }
tasks.register('clean', Delete) { tasks.register('clean', Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View file

@ -1,4 +1,6 @@
# What's Changed # What's Changed
- newpipe extractor general fixes - newpipe extractor general fixes
- fixed newpipe formats not working
- removed piped because its more annoying than anything, it doesnt work currently - removed piped because its more annoying than anything, it doesnt work currently
*shortest changelog wr*

View file

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