From 56b503a46b81923eb3e768cb62124c8a024b184a Mon Sep 17 00:00:00 2001 From: zaednasr <75589932+zaednasr@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:41:07 +0200 Subject: [PATCH] 1.7.9.3 pre --- CHANGELOG.md | 4 +- app/build.gradle | 33 ++-- .../java/com/deniscerri/ytdl/MainActivity.kt | 6 +- .../ytdl/database/dao/DownloadDao.kt | 30 +++- .../ytdl/database/dao/HistoryDao.kt | 12 +- .../database/repository/DownloadRepository.kt | 80 +++++---- .../database/repository/HistoryRepository.kt | 38 ++-- .../database/repository/ResultRepository.kt | 50 +++++- .../database/viewmodel/DownloadViewModel.kt | 149 ++++++++++++--- .../database/viewmodel/HistoryViewModel.kt | 59 +++--- .../database/viewmodel/ResultViewModel.kt | 11 +- .../deniscerri/ytdl/receiver/ShareActivity.kt | 11 +- .../com/deniscerri/ytdl/ui/HomeFragment.kt | 15 +- .../ytdl/ui/adapter/ActiveDownloadAdapter.kt | 46 +++-- .../adapter/ActiveDownloadMinifiedAdapter.kt | 46 ++--- .../ytdl/ui/adapter/TemplatesAdapter.kt | 22 ++- .../downloadcard/ResultCardDetailsDialog.kt | 66 ++++--- .../ui/downloads/ActiveDownloadsFragment.kt | 145 +++++++-------- .../ui/downloads/DownloadQueueMainFragment.kt | 20 +-- .../ytdl/ui/downloads/HistoryFragment.kt | 169 ++++++++++++------ .../ytdl/ui/more/CommandTemplatesFragment.kt | 13 +- .../deniscerri/ytdl/ui/more/MoreFragment.kt | 3 +- .../ui/more/settings/MainSettingsFragment.kt | 8 + .../com/deniscerri/ytdl/util/Extensions.kt | 27 ++- .../com/deniscerri/ytdl/util/FormatUtil.kt | 10 +- .../java/com/deniscerri/ytdl/util/UiUtil.kt | 21 ++- .../ytdl/util/extractors/YTDLPUtil.kt | 42 +++-- .../util/extractors/newpipe/NewPipeUtil.kt | 4 +- .../deniscerri/ytdl/work/DownloadWorker.kt | 41 ++++- .../ytdl/work/ObserveSourceWorker.kt | 2 +- .../res/drawable/baseline_exit_to_app_24.xml | 5 + .../main/res/layout-sw600dp/fragment_home.xml | 3 +- .../main/res/layout/active_download_card.xml | 30 ++-- .../main/res/layout/command_template_item.xml | 76 ++++++-- .../res/layout/create_command_template.xml | 76 ++++---- .../main/res/layout/download_bottom_sheet.xml | 4 +- app/src/main/res/layout/fragment_active.xml | 8 +- .../res/layout/fragment_command_templates.xml | 1 - app/src/main/res/layout/fragment_history.xml | 22 --- .../layout/history_other_filters_sheet.xml | 91 ++++++++++ .../main/res/layout/select_playlist_items.xml | 2 + .../res/menu/active_downloads_minified.xml | 24 +-- app/src/main/res/menu/history_menu.xml | 6 + app/src/main/res/values/strings.xml | 9 +- app/src/main/res/xml/general_preferences.xml | 7 + app/src/main/res/xml/root_preferences.xml | 4 + build.gradle | 14 +- .../android/en-US/changelogs/10792.txt | 2 + gradle/wrapper/gradle-wrapper.properties | 2 +- 49 files changed, 1016 insertions(+), 553 deletions(-) create mode 100644 app/src/main/res/drawable/baseline_exit_to_app_24.xml create mode 100644 app/src/main/res/layout/history_other_filters_sheet.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c2f0f8d..78baeaca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,13 @@ # YTDLnis Changelog -> # 1.7.9.2 +> # 1.7.9.2 (2024-09) # What's Changed - newpipe extractor general fixes +- fixed newpipe formats not working - removed piped because its more annoying than anything, it doesnt work currently + *shortest changelog wr* > # 1.7.9.1 (2024-09) diff --git a/app/build.gradle b/app/build.gradle index 2dee9185..5c1aae1a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,7 +24,7 @@ if (isBeta) { android { namespace 'com.deniscerri.ytdl' - compileSdk 34 + compileSdk 35 try { def propertiesFile = rootProject.file("keystore.properties") @@ -44,8 +44,8 @@ android { }catch(IOException ignored){} defaultConfig { applicationId "com.deniscerri.ytdl" - minSdk 23 - targetSdk 33 + minSdk 24 + targetSdk 35 versionCode versionMajor * 1000000 + versionMinor * 10000 + versionPatch * 100 + versionBuild versionName "${versionMajor}.${versionMinor}.${versionPatch}${versionExt}" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -135,14 +135,15 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - coreLibraryDesugaring "com.android.tools:desugar_jdk_libs_nio:2.1.0" - implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer" - implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer" - implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer" + 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:ffmpeg:$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:aria2c:0.16.1" + + implementation "io.github.junkfood02.youtubedl-android:library:0.17.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.constraintlayout:constraintlayout:2.1.4" @@ -154,7 +155,7 @@ dependencies { implementation "androidx.navigation:navigation-ui-ktx:$navVer" implementation 'androidx.core:core-ktx:1.13.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' testImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer" @@ -176,10 +177,10 @@ dependencies { implementation "androidx.room:room-runtime:$roomVer" implementation "androidx.room:room-ktx:$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" 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" androidTestImplementation 'com.google.truth:truth:1.1.5' @@ -191,7 +192,7 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' 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" // For media playback using ExoPlayer @@ -207,8 +208,8 @@ dependencies { implementation "com.anggrayudi:storage:1.5.5" implementation 'me.zhanghai.android.fastscroll:library:1.3.0' 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("org.greenrobot:eventbus:3.3.1") - implementation("com.github.teamnewpipe:NewPipeExtractor:0.24.2") + implementation("com.github.teamnewpipe:newpipeextractor:v0.24.2") } diff --git a/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt b/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt index 99a24f99..f179a9e6 100644 --- a/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt @@ -16,6 +16,7 @@ import android.os.Handler import android.os.Looper import android.provider.Settings import android.util.Log +import android.util.Patterns import android.view.View import android.view.WindowInsets 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.more.settings.SettingsActivity import com.deniscerri.ytdl.util.CrashListener +import com.deniscerri.ytdl.util.Extensions.extractURL import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.NavbarUtil import com.deniscerri.ytdl.util.NavbarUtil.applyNavBarStyle @@ -183,7 +185,7 @@ class MainActivity : BaseActivity() { (navigationView as NavigationBarView).getOrCreateBadge(R.id.historyFragment) } lifecycleScope.launch { - downloadViewModel.activeDownloadsCount.collectLatest { + downloadViewModel.activePausedDownloadsCount.collectLatest { if (it == 0) { activeDownloadsBadge.isVisible = false activeDownloadsBadge.clearNumber() @@ -438,7 +440,7 @@ class MainActivity : BaseActivity() { textBuilder.append(c.toChar()) } val bundle = Bundle() - bundle.putString("url", textBuilder.toString()) + bundle.putString("url", textBuilder.toString().extractURL()) navController.popBackStack(R.id.homeFragment, true) navController.navigate( R.id.homeFragment, diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt index c532c8b1..337b55ac 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt @@ -26,6 +26,15 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Active'") fun getActiveDownloads() : Flow> + @Query("SELECT * FROM downloads WHERE status='Active' or status = 'Paused' ORDER BY CASE WHEN status = 'Active' THEN 0 ELSE 1 END") + fun getActiveAndPausedDownloads() : Flow> + + @Query("SELECT * FROM downloads WHERE status='Paused'") + fun getPausedDownloads() : Flow> + + @Query("SELECT * FROM downloads WHERE status='Paused'") + fun getPausedDownloadsList() : List + @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM downloads WHERE status = 'Processing'") fun getProcessingDownloads() : Flow> @@ -79,10 +88,11 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')") fun getActiveAndQueuedDownloadsList() : List - @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 @Query("SELECT * FROM downloads WHERE status in('Active','Queued')") @@ -95,7 +105,11 @@ interface DownloadDao { @Query("SELECT format FROM downloads WHERE status='Queued'") fun getSelectedFormatFromQueued() : List - @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> @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)") suspend fun deleteAllWithIDs(list: List) - @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() @Query("DELETE FROM downloads WHERE status='Processing' AND id=:id") @@ -200,7 +214,11 @@ interface DownloadDao { suspend fun updateAll(list: List) : List { val toReturn = mutableListOf() list.forEach { - it.id = update(it) + if (it.id > 0) { + update(it) + }else{ + it.id = insert(it) + } toReturn.add(it) } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/HistoryDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/HistoryDao.kt index a07fbdf3..f9fab094 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/dao/HistoryDao.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/HistoryDao.kt @@ -11,23 +11,23 @@ import kotlinx.coroutines.flow.Flow @Dao 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 = 'DESC' THEN id END DESC," + "CASE WHEN :sort = '' THEN id END DESC ") - fun getHistorySortedByID(query : String, format : String, site : String, sort : String) : List + fun getHistorySortedByID(query : String, type : String, site : String, sort : String) : List - @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 = 'DESC' THEN title END DESC," + "CASE WHEN :sort = '' THEN title END DESC ") - fun getHistorySortedByTitle(query : String, format : String, site : String, sort : String) : List + fun getHistorySortedByTitle(query : String, type : String, site : String, sort : String) : List - @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 = 'DESC' THEN author END DESC," + "CASE WHEN :sort = '' THEN author END DESC ") - fun getHistorySortedByAuthor(query : String, format : String, site : String, sort : String) : List + fun getHistorySortedByAuthor(query : String, type : String, site : String, sort : String) : List @Query("SELECT * FROM history") diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt index e23931af..729ee416 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt @@ -44,6 +44,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) { pagingSourceFactory = {downloadDao.getAllDownloads()} ) val activeDownloads : Flow> = downloadDao.getActiveDownloads().distinctUntilChanged() + val activePausedDownloads : Flow> = downloadDao.getActiveAndPausedDownloads().distinctUntilChanged() + val pausedDownloads : Flow> = downloadDao.getPausedDownloads().distinctUntilChanged() val processingDownloads : Flow> = downloadDao.getProcessingDownloads().distinctUntilChanged() val queuedDownloads : Pager = Pager( config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), @@ -67,15 +69,16 @@ class DownloadRepository(private val downloadDao: DownloadDao) { ) val activeDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active).toListString()) + val activePausedDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.Paused).toListString()) val queuedDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Queued).toListString()) - val activeQueuedDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.Queued).toListString()) + val pausedDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Paused).toListString()) val cancelledDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Cancelled).toListString()) val erroredDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Error).toListString()) val savedDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Saved).toListString()) val scheduledDownloadsCount : Flow = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Scheduled).toListString()) 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 { @@ -222,48 +225,47 @@ class DownloadRepository(private val downloadDao: DownloadDao) { } @SuppressLint("RestrictedApi") - suspend fun startDownloadWorker(queuedItems: List, context: Context, inputData: Data.Builder = Data.Builder()) : Result { + fun startDownloadWorker(queuedItems: List, context: Context, continueAfterPriorityItems: Boolean = true) : Result { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true) val workManager = WorkManager.getInstance(context) - val currentWork = workManager.getWorkInfosByTag("download").await() - if (currentWork.size == 0 || currentWork.none{ it.state == WorkInfo.State.RUNNING } || (queuedItems.isNotEmpty() && queuedItems[0].downloadStartTime != 0L)){ - - val currentTime = System.currentTimeMillis() - var delay = 0L - if (queuedItems.isNotEmpty()){ - val earliestStart = queuedItems.minBy { it.downloadStartTime } - delay = if (earliestStart.downloadStartTime != 0L){ - earliestStart.downloadStartTime.minus(currentTime) - } else 0 - 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() - .addTag("download") - .setConstraints(workConstraints.build()) - .setInitialDelay(delay, TimeUnit.MILLISECONDS) - .setInputData(inputData.build()) - - workManager.enqueueUniqueWork( - System.currentTimeMillis().toString(), - ExistingWorkPolicy.REPLACE, - workRequest.build() - ) - + val inputData = Data.Builder() + val currentTime = System.currentTimeMillis() + var delay = 0L + if (queuedItems.isNotEmpty()){ + val earliestStart = queuedItems.minBy { it.downloadStartTime } + delay = if (earliestStart.downloadStartTime != 0L){ + earliestStart.downloadStartTime.minus(currentTime) + } else 0 + if (delay <= 60000L) delay = 0L + inputData.putLongArray("priority_item_ids", queuedItems.map { it.id }.toLongArray()) } + + 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() + .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 isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/HistoryRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/HistoryRepository.kt index 74da9373..2ade553c 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/HistoryRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/HistoryRepository.kt @@ -3,6 +3,7 @@ package com.deniscerri.ytdl.database.repository import com.deniscerri.ytdl.database.DBManager.SORTING import com.deniscerri.ytdl.database.dao.HistoryDao import com.deniscerri.ytdl.database.models.HistoryItem +import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.util.FileUtil import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest @@ -26,41 +27,28 @@ class HistoryRepository(private val historyDao: HistoryDao) { return historyDao.getAllHistoryByURL(url) } - fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, notDeleted: Boolean) : List { + fun getFiltered(query : String, type : String, site : String, sortType: HistorySortType, sort: SORTING, statusFilter: HistoryViewModel.HistoryStatus) : List { 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.DATE -> historyDao.getHistorySortedByID(query, type, site, sort.toString()) + HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, type, site, sort.toString()) + HistorySortType.AUTHOR -> historyDao.getHistorySortedByAuthor(query, type, site, sort.toString()) HistorySortType.FILESIZE -> { - val items = historyDao.getHistorySortedByID(query, format, site, sort.toString()) + val items = historyDao.getHistorySortedByID(query, type, site, sort.toString()) when(sort){ SORTING.DESC -> items.sortedByDescending { it.format.filesize } SORTING.ASC -> items.sortedBy { it.format.filesize } } } } - if(notDeleted){ - filtered = filtered.filter { it.downloadPath.any { it2 -> FileUtil.exists(it2) } } - } - return filtered - } - - fun getRecordsBetweenTwoItems(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, notDeleted: Boolean) : List { - 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 } - } + when(statusFilter) { + HistoryViewModel.HistoryStatus.DELETED -> { + filtered = filtered.filter { it.downloadPath.any { it2 -> !FileUtil.exists(it2) } } } - } - if(notDeleted){ - filtered = filtered.filter { it.downloadPath.any { it2 -> FileUtil.exists(it2) } } + HistoryViewModel.HistoryStatus.NOT_DELETED -> { + filtered = filtered.filter { it.downloadPath.any { it2 -> FileUtil.exists(it2) } } + } + else -> {} } return filtered diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt index efeefee0..8dec814e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt @@ -14,12 +14,14 @@ import com.deniscerri.ytdl.database.repository.HistoryRepository.HistorySortType import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.util.Extensions.isYoutubeChannelURL 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.extractors.GoogleApiUtil import com.deniscerri.ytdl.util.extractors.PipedApiUtil import com.deniscerri.ytdl.util.extractors.newpipe.NewPipeUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil import com.deniscerri.ytdl.util.extractors.YoutubeApiUtil +import com.yausername.youtubedl_android.YoutubeDLException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runBlocking @@ -46,6 +48,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co enum class SourceType { YOUTUBE_VIDEO, + YOUTUBE_WATCHVIDEOS, YOUTUBE_PLAYLIST, YOUTUBE_CHANNEL, SEARCH_QUERY, @@ -111,6 +114,40 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co return items } + private suspend fun getYoutubeWatchVideos(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : List { + 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() + 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{ val theURL = inputQuery.replace("\\?list.*".toRegex(), "") val ytExtractorRes = newPipeUtil?.getVideoData(theURL) @@ -157,9 +194,11 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co ytExtractorResult.getOrElse { items } }else{ val res = ytdlpUtil.getFromYTDL(playlistURL) - val ids = resultDao.insertMultiple(res) - ids.forEachIndexed { index, id -> - res[index].id = id + if (addToResults) { + val ids = resultDao.insertMultiple(res) + ids.forEachIndexed { index, id -> + res[index].id = id + } } res } @@ -313,6 +352,9 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co SourceType.YOUTUBE_VIDEO -> { getYoutubeVideo(inputQuery, resetResults, addToResults) } + SourceType.YOUTUBE_WATCHVIDEOS -> { + getYoutubeWatchVideos(inputQuery, resetResults, addToResults) + } SourceType.YOUTUBE_PLAYLIST -> { if (singleItem){ getFromYTDLP(inputQuery, resetResults, addToResults, true) @@ -345,6 +387,8 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co type = SourceType.YOUTUBE_PLAYLIST }else if (inputQuery.isYoutubeChannelURL()) { type = SourceType.YOUTUBE_CHANNEL + }else if (inputQuery.isYoutubeWatchVideosURL()) { + type = SourceType.YOUTUBE_WATCHVIDEOS } } else if (Patterns.WEB_URL.matcher(inputQuery).matches()) { type = SourceType.YT_DLP diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt index 713987eb..98fcd8dc 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt @@ -9,9 +9,13 @@ import android.os.Parcelable import android.util.DisplayMetrics import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import androidx.paging.PagingData +import androidx.paging.filter +import androidx.paging.map import androidx.preference.PreferenceManager import androidx.work.Data 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.FileUtil import com.deniscerri.ytdl.util.FormatUtil +import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker import com.google.gson.Gson +import com.yausername.youtubedl_android.YoutubeDL import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow 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.runBlocking import kotlinx.coroutines.withContext @@ -62,12 +73,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel private val sharedPreferences: SharedPreferences private val commandTemplateDao: CommandTemplateDao private val formatUtil = FormatUtil(application) + private val notificationUtil = NotificationUtil(application) private val ytdlpUtil = YTDLPUtil(application) private val resources : Resources val allDownloads : Flow> val queuedDownloads : Flow> val activeDownloads : Flow> + val activePausedDownloads : Flow> val processingDownloads : Flow> val cancelledDownloads : Flow> val erroredDownloads : Flow> @@ -75,13 +88,21 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val scheduledDownloads : Flow> val activeDownloadsCount : Flow + val activePausedDownloadsCount : Flow val queuedDownloadsCount : Flow - val activeQueuedDownloadsCount : Flow + val pausedDownloadsCount : Flow val cancelledDownloadsCount : Flow val erroredDownloadsCount : Flow val savedDownloadsCount : Flow val scheduledDownloadsCount : Flow + val pausedAllDownloads = MediatorLiveData(PausedAllDownloadsState.HIDDEN) + private val pausedAllDownloadsFlow : Flow + private var isPausingResuming = false + enum class PausedAllDownloadsState { + PAUSE, RESUME, PROCESSING, HIDDEN + } + @Parcelize data class AlreadyExistsIDs( var downloadItemID: Long, @@ -122,8 +143,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel commandTemplateDao = DBManager.getInstance(application).commandTemplateDao activeDownloadsCount = repository.activeDownloadsCount + activePausedDownloadsCount = repository.activePausedDownloadsCount queuedDownloadsCount = repository.queuedDownloadsCount - activeQueuedDownloadsCount = repository.activeQueuedDownloadsCount + pausedDownloadsCount = repository.pausedDownloadsCount cancelledDownloadsCount = repository.cancelledDownloadsCount erroredDownloadsCount = repository.erroredDownloadsCount savedDownloadsCount = repository.savedDownloadsCount @@ -132,6 +154,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel allDownloads = repository.allDownloads.flow queuedDownloads = repository.queuedDownloads.flow activeDownloads = repository.activeDownloads + activePausedDownloads = repository.activePausedDownloads processingDownloads = repository.processingDownloads savedDownloads = repository.savedDownloads.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 locale = if (Build.VERSION.SDK_INT < 33) { @@ -496,13 +538,20 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel ) } else -> { - val lastUsedCommandTemplate = sharedPreferences.getString("lastCommandTemplateUsed", "")!! - val c = if (lastUsedCommandTemplate.isBlank()){ - commandTemplateDao.getFirst() ?: CommandTemplate(0,"","", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false) - }else{ - commandTemplateDao.getTemplateByContent(lastUsedCommandTemplate) ?: CommandTemplate(0, "", lastUsedCommandTemplate, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false) + val preferredTemplateContent = + sharedPreferences.getString("preferred_command_template", "")!!.ifEmpty { + sharedPreferences.getString("lastCommandTemplateUsed", "")!! + } + + 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) } - fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) { + private fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) { processingItemsJob?.apply { cancel(CancellationException()) } repository.cancelActiveQueued() } @@ -705,14 +754,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel repository.startDownloadWorker(emptyList(), application) } - suspend fun resetActiveToQueued() { - dbManager.downloadDao.resetActiveToQueued() - } - - suspend fun startDownloadWorker(list: List){ - repository.startDownloadWorker(list, application) - } - suspend fun putAtTopOfQueue(ids: List) = CoroutineScope(Dispatchers.IO).launch{ val downloads = dao.getQueuedDownloadsListIDs() val lastID = ids.maxOf { it } @@ -809,11 +850,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel items.forEach { if (it.downloadStartTime > 0) { it.status = DownloadRepository.Status.Scheduled.toString() - }else{ + }else { it.status = DownloadRepository.Status.Queued.toString() } - //CHECK DUPLICATES var isDuplicate = false if (checkDuplicate.isNotEmpty() && !ignoreDuplicates){ @@ -939,11 +979,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } }else{ val queued = repository.updateAll(queuedItems) - - if (sharedPreferences.getBoolean("paused_downloads", true)) { - sharedPreferences.edit().putBoolean("paused_downloads", false).apply() - resetActiveToQueued() - } + println(queued.size) 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) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/HistoryViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/HistoryViewModel.kt index df63c93b..12d934af 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/HistoryViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/HistoryViewModel.kt @@ -10,9 +10,17 @@ import androidx.lifecycle.viewModelScope import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.DBManager.SORTING 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.HistorySortType +import com.deniscerri.ytdl.util.Extensions 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 class HistoryViewModel(application: Application) : AndroidViewModel(application) { @@ -20,36 +28,40 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application) val sortOrder = MutableLiveData(SORTING.DESC) val sortType = MutableLiveData(HistorySortType.DATE) val websiteFilter = MutableLiveData("") + val statusFilter = MutableLiveData(HistoryStatus.UNSET) private val queryFilter = MutableLiveData("") - private val formatFilter = MutableLiveData("") - private val notDeletedFilter = MutableLiveData(false) + private val typeFilter = MutableLiveData("") + + enum class HistoryStatus { + UNSET, DELETED, NOT_DELETED, ALL + } val allItems : LiveData> private var _items = MediatorLiveData>() + init { val dao = DBManager.getInstance(application).historyDao repository = HistoryRepository(dao) allItems = repository.items.asLiveData() - _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){ - filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!) + _items.addSource(typeFilter){ + filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!) } _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){ - 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){ - 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){ - filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!) + _items.addSource(statusFilter){ + 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 } - fun setFormatFilter(filter: String){ - formatFilter.value = filter + fun setTypeFilter(filter: String){ + typeFilter.value = filter } - fun setNotDeleted(filter: Boolean){ - notDeletedFilter.value = filter + fun setStatusFilter(status: HistoryStatus) { + statusFilter.value = status } - private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, notDeleted: Boolean) = viewModelScope.launch(Dispatchers.IO){ - _items.postValue(repository.getFiltered(query, format, site, sortType, sort, notDeleted)) + 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, statusFilter)) } fun getAll() : List { return repository.getAll() } + fun getByID(id: Long) : HistoryItem { + return repository.getItem(id) + } + fun insert(item: HistoryItem) = viewModelScope.launch(Dispatchers.IO){ repository.insert(item) } @@ -118,13 +134,14 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application) } fun getRecordsBetweenTwoItems(item1: Long, item2: Long) : List { - 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 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{ - return filtered.filterIndexed { index, _ -> index in (firstIndex + 1) until secondIndex } + filtered.filterIndexed { index, _ -> index in (firstIndex + 1) until secondIndex } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt index a8a7b0da..afe7720a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.withContext import java.util.concurrent.CancellationException @@ -96,6 +97,14 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a playlistFilter.value = p } + private fun resetPlaylistFilter() = viewModelScope.launch(Dispatchers.Main) { + _items.removeSource(playlistFilter) + playlistFilter.value = "" + _items.addSource(playlistFilter) { + filter() + } + } + fun getFilteredList() : LiveData> { return _items } @@ -133,7 +142,6 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a repository.itemCount.value = inputQueries.size } val resetResults = inputQueries.size == 1 - uiState.update {it.copy(processing = true, errorMessage = null)} val res = mutableListOf() @@ -195,6 +203,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a suspend fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { repository.deleteAll() + resetPlaylistFilter() } fun update(item: ResultItem) = viewModelScope.launch(Dispatchers.IO){ diff --git a/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt b/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt index 83190253..7788ee99 100644 --- a/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt @@ -39,6 +39,7 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.ui.BaseActivity +import com.deniscerri.ytdl.util.Extensions.extractURL import com.deniscerri.ytdl.util.ThemeUtil import com.deniscerri.ytdl.util.UiUtil import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -170,16 +171,12 @@ class ShareActivity : BaseActivity() { runCatching { supportFragmentManager.popBackStack() } 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)!! else -> intent.dataString!! } - val matcher = Patterns.WEB_URL.matcher(url) - val inputQuery = if (matcher.find()){ - matcher.group() - }else{ - url - } + + val inputQuery = data.extractURL() val type = intent.getStringExtra("TYPE") val background = intent.getBooleanExtra("BACKGROUND", false) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt index 3273cd66..18b4f5f9 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt @@ -981,12 +981,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti searchView!!.editText.setSelection(searchView!!.editText.length()) } + private fun updateMultiplePlaylistResults(playlistTitles: List) { + playlistNameFilterChipGroup.children.filter { it.tag != "all" }.forEach { + playlistNameFilterChipGroup.removeView(it) + } + if (playlistTitles.isEmpty() || playlistTitles.size == 1) { playlistNameFilterScrollView.isVisible = false - playlistNameFilterChipGroup.children.filter { it.tag != "all" }.forEach { - playlistNameFilterChipGroup.removeView(it) - } return } @@ -995,9 +997,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti } 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 tmp.text = t tmp.tag = t @@ -1008,6 +1007,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti playlistNameFilterChipGroup.addView(tmp) } + if (playlistNameFilterChipGroup.children.all { !(it as Chip).isChecked }) { + (playlistNameFilterChipGroup.children.first() as Chip).isChecked = true + } + playlistNameFilterScrollView.isVisible = true } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadAdapter.kt index 946ff86f..4319d5fc 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadAdapter.kt @@ -71,8 +71,6 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: // PROGRESS BAR ---------------------------------------------------- val progressBar = card.findViewById(R.id.progress) progressBar.tag = "${item.id}##progress" - progressBar.progress = 0 - progressBar.isIndeterminate = true // TITLE ---------------------------------- val itemTitle = card.findViewById(R.id.title) @@ -122,39 +120,39 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null) cancelButton.setOnClickListener {onItemClickListener.onCancelClick(item.id)} -// val resumeButton = card.findViewById(R.id.active_download_resume) -// resumeButton.isEnabled = true -// if (resumeButton.hasOnClickListeners()) resumeButton.setOnClickListener(null) -// if (activePaused) { -// resumeButton.setIconResource(R.drawable.exomedia_ic_play_arrow_white) -// resumeButton.setOnClickListener { -// resumeButton.isEnabled = false -// onItemClickListener.onResumeClick(item.id) -// } -// }else { -// resumeButton.setIconResource(R.drawable.exomedia_ic_pause_white) -// resumeButton.setOnClickListener { -// resumeButton.isEnabled = false -// onItemClickListener.onPauseClick(item.id) -// } -// } + val resumeButton = card.findViewById(R.id.active_download_resume) + resumeButton.isEnabled = true + if (resumeButton.hasOnClickListeners()) resumeButton.setOnClickListener(null) + val isPaused = item.status == DownloadRepository.Status.Paused.toString() + if (isPaused) { + resumeButton.setIconResource(R.drawable.exomedia_ic_play_arrow_white) + resumeButton.setOnClickListener { + resumeButton.isEnabled = false + onItemClickListener.onResumeClick(item.id) + } + }else { + resumeButton.setIconResource(R.drawable.exomedia_ic_pause_white) + resumeButton.setOnClickListener { + resumeButton.isEnabled = false + onItemClickListener.onPauseClick(item.id) + } + } - if (sharedPreferences.getBoolean("paused_downloads", false)) { + if (isPaused) { progressBar.isIndeterminate = false + progressBar.progress = 0 cancelButton.isEnabled = true output.text = activity.getString(R.string.exo_download_paused) }else{ - progressBar.isIndeterminate = true + progressBar.isIndeterminate = progressBar.progress <= 0 cancelButton.isEnabled = true } } interface OnItemClickListener { fun onCancelClick(itemID: Long) fun onOutputClick(item: DownloadItem) - } - - enum class ActiveDownloadAction { - Resume, Pause + fun onPauseClick(itemID: Long) + fun onResumeClick(itemID: Long) } companion object { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt index c7421c4b..f13e41fd 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt @@ -129,35 +129,35 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac else fileSize.text = fileSizeReadable val menu = card.findViewById(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 { val popup = PopupMenu(activity, it) popup.menuInflater.inflate(R.menu.active_downloads_minified, popup.menu) if (Build.VERSION.SDK_INT > 27) popup.menu.setGroupDividerEnabled(true) -// val pause = popup.menu[0] -// val resume = popup.menu[1] -// -// if (paused){ -// pause.isVisible = false -// resume.isVisible = true -// }else{ -// pause.isVisible = true -// resume.isVisible = false -// } + val pause = popup.menu[0] + val resume = popup.menu[1] + + if (paused){ + pause.isVisible = false + resume.isVisible = true + }else{ + pause.isVisible = true + resume.isVisible = false + } popup.setOnMenuItemClickListener { m -> when(m.itemId){ -// R.id.pause -> { -// onItemClickListener.onPauseClick(item.id, ActiveDownloadAdapter.ActiveDownloadAction.Pause, position) -// if (progressBar.progress == 0) progressBar.isIndeterminate = false -// popup.dismiss() -// } -// R.id.resume -> { -// onItemClickListener.onPauseClick(item.id, ActiveDownloadAdapter.ActiveDownloadAction.Resume, position) -// progressBar.isIndeterminate = true -// popup.dismiss() -// } + R.id.pause -> { + onItemClickListener.onPauseClick(item.id, position) + if (progressBar.progress == 0) progressBar.isIndeterminate = false + popup.dismiss() + } + R.id.resume -> { + onItemClickListener.onResumeClick(item.id, position) + progressBar.isIndeterminate = true + popup.dismiss() + } R.id.cancel -> { onItemClickListener.onCancelClick(item.id) popup.dismiss() @@ -170,7 +170,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac } - progressBar.isIndeterminate = !sharedPreferences.getBoolean("paused_downloads", false) + progressBar.isIndeterminate = !paused card.setOnClickListener { onItemClickListener.onCardClick() @@ -178,6 +178,8 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac } interface OnItemClickListener { fun onCancelClick(itemID: Long) + fun onPauseClick(itemID: Long, position: Int) + fun onResumeClick(itemID: Long, position: Int) fun onCardClick() } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/TemplatesAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/TemplatesAdapter.kt index e01faaa5..fbee6208 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/TemplatesAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/TemplatesAdapter.kt @@ -2,11 +2,16 @@ package com.deniscerri.ytdl.ui.adapter import android.annotation.SuppressLint 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.View import android.view.ViewGroup import android.widget.TextView import androidx.core.view.isVisible +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter @@ -22,10 +27,12 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ private val onItemClickListener: OnItemClickListener private val activity: Activity private val checkedItems: ArrayList = ArrayList() + private val sharedPreferences: SharedPreferences init { this.onItemClickListener = onItemClickListener this.activity = activity + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) } 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) { - val item = getItem(position) + val item = getItem(position)!! val card = holder.item card.popup() val title = card.findViewById(R.id.title) - title.text = item?.title + title.text = item.title val content = card.findViewById(R.id.content) - content.text = item?.content + content.text = item.content card.findViewById(R.id.useInExtraCommands).apply { - isVisible = item!!.useAsExtraCommand + isVisible = item.useAsExtraCommand 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 finalText = context.getString(R.string.extra_command) + " " + listOfNotNull( @@ -64,7 +71,12 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ text = finalText } - if (checkedItems.contains(item!!.id)) { + card.findViewById(R.id.preferredTemplate).apply { + val preferred = sharedPreferences.getString("preferred_command_template", "") + isVisible = preferred == item.content + } + + if (checkedItems.contains(item.id)) { card.isChecked = true card.strokeWidth = 5 } else { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt index 8cbb1f98..2df12f00 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt @@ -20,8 +20,10 @@ import android.view.ViewGroup import android.view.Window import android.widget.Button import android.widget.TextView +import android.widget.Toast import androidx.core.net.toUri import androidx.core.os.bundleOf +import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.media3.common.MediaItem @@ -240,16 +242,21 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap true } + downloadThumb.isVisible = item.thumb.isNotBlank() downloadThumb.setOnClickListener { - downloadManager.enqueue( - DownloadManager.Request(item.thumb.toUri()) - .setAllowedNetworkTypes( - DownloadManager.Request.NETWORK_WIFI or - DownloadManager.Request.NETWORK_MOBILE - ) - .setAllowedOverRoaming(true) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/" + item.title + ".jpg")) + runCatching { + downloadManager.enqueue( + DownloadManager.Request(item.thumb.toUri()) + .setAllowedNetworkTypes( + DownloadManager.Request.NETWORK_WIFI or + DownloadManager.Request.NETWORK_MOBILE + ) + .setAllowedOverRoaming(true) + .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 @@ -479,33 +486,24 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap withContext(Dispatchers.IO){ 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() { this.dismiss() findNavController().navigate( diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ActiveDownloadsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ActiveDownloadsFragment.kt index 03f8a0f8..b16c20e7 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ActiveDownloadsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ActiveDownloadsFragment.kt @@ -5,9 +5,11 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.DialogInterface import android.content.SharedPreferences +import android.content.res.ColorStateList import android.graphics.Canvas import android.graphics.Color import android.os.Bundle +import android.util.TypedValue import android.view.LayoutInflater import android.view.Menu import android.view.MenuItem @@ -23,6 +25,8 @@ import androidx.annotation.OptIn import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.core.view.marginBottom @@ -36,6 +40,7 @@ import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView +import androidx.room.util.getColumnIndexOrThrow import androidx.work.WorkManager import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdl.R @@ -86,8 +91,8 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis private lateinit var activeDownloads : ActiveDownloadAdapter private lateinit var notificationUtil: NotificationUtil - private lateinit var pause: ExtendedFloatingActionButton - private lateinit var resume: ExtendedFloatingActionButton + private lateinit var pauseBtn: ExtendedFloatingActionButton + private lateinit var resumeBtn: ExtendedFloatingActionButton private lateinit var noResults: RelativeLayout private lateinit var workManager: WorkManager private lateinit var preferences: SharedPreferences @@ -119,80 +124,64 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis activeRecyclerView.adapter = activeDownloads activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) - pause = view.findViewById(R.id.pause) - pause.isVisible = false - resume = view.findViewById(R.id.resume) + pauseBtn = view.findViewById(R.id.pause) + pauseBtn.isVisible = false + resumeBtn = view.findViewById(R.id.resume) noResults = view.findViewById(R.id.no_results) - pause.setOnClickListener { + downloadViewModel.pausedAllDownloads.observe(viewLifecycleOwner) { value -> lifecycleScope.launch { - pause.isEnabled = false - delay(1000) - workManager.cancelAllWorkByTag("download") - val activeDownloadsList = withContext(Dispatchers.IO){ - downloadViewModel.getActiveDownloads() - } - - activeDownloadsList.forEach { - 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){ + when(value) { + DownloadViewModel.PausedAllDownloadsState.PAUSE -> { + resumeBtn.isVisible = false + pauseBtn.isClickable = true + val typedValue = TypedValue() + requireContext().theme.resolveAttribute(R.attr.colorPrimaryContainer, typedValue, true) + pauseBtn.backgroundTintList = ColorStateList.valueOf(typedValue.data) + pauseBtn.isVisible = true 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 { - val activeCount = withContext(Dispatchers.IO) { - downloadViewModel.getActiveDownloadsCount() + pauseBtn.setOnClickListener { + lifecycleScope.launch { + 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 { - downloadViewModel.activeQueuedDownloadsCount.collectLatest { - if (it == 0) { - pause.isVisible = false - resume.isVisible = false - } + resumeBtn.setOnClickListener { + lifecycleScope.launch { + downloadViewModel.resumeAllDownloads() } } lifecycleScope.launch { - downloadViewModel.activeDownloads.collectLatest { + downloadViewModel.activePausedDownloads.collectLatest { noResults.isVisible = it.isEmpty() activeDownloads.submitList(it) + activeRecyclerView.scrollTo(0,0) } } @@ -235,32 +224,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis withContext(Dispatchers.IO){ 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) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/DownloadQueueMainFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/DownloadQueueMainFragment.kt index 66f3383f..828e244e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/DownloadQueueMainFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/DownloadQueueMainFragment.kt @@ -209,7 +209,7 @@ class DownloadQueueMainFragment : Fragment(){ when(m.itemId){ R.id.clear_queue -> { showDeleteDialog { - cancelAllDownloads() + downloadViewModel.cancelAllDownloads() } } R.id.clear_cancelled -> { @@ -270,24 +270,6 @@ class DownloadQueueMainFragment : Fragment(){ 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(){ tabLayout.getTabAt(0)!!.select() viewPager2.setCurrentItem(0, true) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/HistoryFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/HistoryFragment.kt index 0e395778..6a1d013f 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/HistoryFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/HistoryFragment.kt @@ -29,7 +29,9 @@ import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat import androidx.core.os.bundleOf +import androidx.core.view.children import androidx.core.view.forEach +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider 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.HistoryViewModel 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.isYoutubeURL import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.NavbarUtil import com.deniscerri.ytdl.util.UiUtil @@ -85,9 +89,9 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ private var uiHandler: Handler? = null private var noResults: RelativeLayout? = null private var selectionChips: LinearLayout? = null - private var websiteGroup: ChipGroup? = null private var historyList: List? = null private var allhistoryList: List? = null + private var websiteList: MutableList = mutableListOf() private lateinit var selectedObjects: ArrayList private var actionMode : ActionMode? = null @@ -112,7 +116,6 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ topAppBar = view.findViewById(R.id.history_toolbar) noResults = view.findViewById(R.id.no_results) selectionChips = view.findViewById(R.id.history_selection_chips) - websiteGroup = view.findViewById(R.id.website_chip_group) uiHandler = Handler(Looper.getMainLooper()) sortChip = view.findViewById(R.id.sortChip) selectedObjects = arrayListOf() @@ -127,7 +130,6 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ historyList = mutableListOf() - allhistoryList = mutableListOf() historyAdapter = HistoryAdapter( @@ -149,14 +151,14 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java] historyViewModel.allItems.observe(viewLifecycleOwner) { - allhistoryList = it if(it.isEmpty()){ noResults!!.visibility = VISIBLE selectionChips!!.visibility = GONE - websiteGroup!!.removeAllViews() + topAppBar!!.menu.children.firstOrNull { m -> m.itemId == R.id.filters }?.isVisible = false }else{ noResults!!.visibility = GONE selectionChips!!.visibility = VISIBLE + topAppBar!!.menu.children.firstOrNull { m -> m.itemId == R.id.filters }?.isVisible = true updateWebsiteChips(it) } } @@ -306,6 +308,106 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ 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(R.id.not_deleted)!! + val deleted = filterSheet.findViewById(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(R.id.websiteFilters)?.isVisible = false + }else{ + val websiteGroup = filterSheet.findViewById(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 } @@ -374,80 +476,43 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ val audio = fragmentView!!.findViewById(R.id.audio_chip) audio.setOnClickListener { if (audio.isChecked) { - historyViewModel.setFormatFilter("audio") + historyViewModel.setTypeFilter(DownloadViewModel.Type.audio.toString()) audio.isChecked = true } else { - historyViewModel.setFormatFilter("") + historyViewModel.setTypeFilter("") audio.isChecked = false } } val video = fragmentView!!.findViewById(R.id.video_chip) video.setOnClickListener { if (video.isChecked) { - historyViewModel.setFormatFilter("video") + historyViewModel.setTypeFilter(DownloadViewModel.Type.video.toString()) video.isChecked = true } else { - historyViewModel.setFormatFilter("") + historyViewModel.setTypeFilter("") video.isChecked = false } } val command = fragmentView!!.findViewById(R.id.command_chip) command.setOnClickListener { if (command.isChecked) { - historyViewModel.setFormatFilter("command") + historyViewModel.setTypeFilter(DownloadViewModel.Type.command.toString()) command.isChecked = true } else { - historyViewModel.setFormatFilter("") + historyViewModel.setTypeFilter("") command.isChecked = false } } - val notDeleted = fragmentView!!.findViewById(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) { - val websites = mutableListOf() - val websiteFilter = historyViewModel.websiteFilter.value + websiteList = mutableListOf() 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(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(R.id.website_divider).visibility = VISIBLE } override fun onCardClick(itemID: Long, isPresent: Boolean) { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/CommandTemplatesFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/CommandTemplatesFragment.kt index 91990160..d700c4ea 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/CommandTemplatesFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/CommandTemplatesFragment.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.ClipboardManager import android.content.Context import android.content.DialogInterface +import android.content.SharedPreferences import android.graphics.Canvas import android.graphics.Color import android.os.Bundle @@ -23,6 +24,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider 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.MaterialToolbar 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.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar @@ -64,6 +67,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene private lateinit var mainActivity: MainActivity private lateinit var sortChip: Chip private lateinit var selectedObjects: ArrayList + private lateinit var preferences: SharedPreferences private var actionMode : ActionMode? = null private val jsonFormat = Json { prettyPrint = true } @@ -95,7 +99,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene recyclerView = view.findViewById(R.id.template_recyclerview) recyclerView.layoutManager = LinearLayoutManager(context) 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")){ val itemTouchHelper = ItemTouchHelper(simpleCallback) itemTouchHelper.attachToRecyclerView(recyclerView) @@ -117,8 +121,8 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene commandTemplateViewModel.sortOrder.observe(viewLifecycleOwner){ if (it != null){ when(it){ - SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down) - SORTING.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up) + SORTING.ASC -> 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) { UiUtil.showCommandTemplateCreationOrUpdatingSheet(commandTemplate,mainActivity, this, commandTemplateViewModel, newTemplate = { - templatesAdapter.notifyItemChanged(index) + templatesAdapter.notifyDataSetChanged() }, {}) } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/MoreFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/MoreFragment.kt index 1d329075..62162b40 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/MoreFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/MoreFragment.kt @@ -145,14 +145,13 @@ class MoreFragment : Fragment() { } activeDownloadsList.forEach { - it.status = DownloadRepository.Status.Queued.toString() YoutubeDL.getInstance().destroyProcessById(it.id.toString()) notificationUtil.cancelDownloadNotification(it.id.toInt()) + it.status = DownloadRepository.Status.Paused.toString() withContext(Dispatchers.IO) { downloadViewModel.updateDownload(it) } } - mainSharedPreferencesEditor.putBoolean("paused_downloads", true).apply() } if (doNotShowAgain){ diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/MainSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/MainSettingsFragment.kt index d0c7f679..4c501a82 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/MainSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/MainSettingsFragment.kt @@ -148,6 +148,14 @@ class MainSettingsFragment : PreferenceFragmentCompat() { backup = findPreference("backup") restore = findPreference("restore") + findPreference("package_name")?.apply { + summary = BuildConfig.APPLICATION_ID + setOnPreferenceClickListener { + UiUtil.copyToClipboard(summary.toString(), requireActivity()) + true + } + } + backup!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { val builder = MaterialAlertDialogBuilder(requireContext()) diff --git a/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt b/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt index 9c8a349d..06ed93ea 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt @@ -20,6 +20,7 @@ import android.net.Uri import android.text.Html import android.text.Spanned import android.util.DisplayMetrics +import android.util.Patterns import android.util.TypedValue import android.view.MotionEvent 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.repository.DownloadRepository 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.toTimePeriodsArray import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog 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.util.listener.HighlightTextWatcher import com.neo.highlight.util.scheme.ColorScheme import com.squareup.picasso.Picasso +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json import me.zhanghai.android.fastscroll.FastScrollerBuilder import org.json.JSONObject import java.io.File @@ -212,7 +219,13 @@ object Extensions { tags.forEach { runCatching { val tmp = this.getString(it) - if (tmp != "null") return tmp + if (tmp != "null") { + return if (tmp.startsWith("[")) { + Json.decodeFromString>(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() } + 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 + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/util/FormatUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/FormatUtil.kt index 45b08a0d..790ae3cc 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/FormatUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/FormatUtil.kt @@ -8,6 +8,7 @@ import androidx.preference.PreferenceManager import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.Format +import java.util.Collections class FormatUtil(private var context: 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 audioContainerPreference : String = sharedPreferences.getString("audio_format", "").toString() private val videoContainerPreference : String = sharedPreferences.getString("video_format", "").toString() - @SuppressLint("RestrictedApi") - private val videoResolutionOrder = context.getStringArray(R.array.video_formats_values).filter { it.contains("_") }.map{ it.split("_")[0].dropLast(1) }.reversed() + private val videoResolutionOrder = context.getStringArray(R.array.video_formats_values) + .filter { it.contains("_") } + .map{ it.split("_")[0].dropLast(1) }.toMutableList().apply { + this.reverse() + } + + private val preferSmallerFormats = sharedPreferences.getBoolean("prefer_smaller_formats", false) @SuppressLint("RestrictedApi") diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt index d8025513..ae27e023 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -182,9 +182,12 @@ object UiUtil { bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) bottomSheet.setContentView(R.layout.create_command_template) + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + val ok : Button = bottomSheet.findViewById(R.id.template_create)!! val title : TextInputLayout = bottomSheet.findViewById(R.id.title)!! 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 extraCommandsAudio : CheckBox = bottomSheet.findViewById(R.id.checkbox_audio)!! val extraCommandsVideo : CheckBox = bottomSheet.findViewById(R.id.checkbox_video)!! @@ -248,6 +251,8 @@ object UiUtil { } if (item != null){ + preferredCommandSwitch.isChecked = item.content == sharedPreferences.getString("preferred_command_template", "") + extraCommandsSwitch.isChecked = item.useAsExtraCommand if (item.useAsExtraCommand){ extraCommandsAudio.isVisible = true @@ -294,6 +299,13 @@ object UiUtil { } 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){ val t = CommandTemplate(0, title.editText!!.text.toString(), content.editText!!.text.toString(), extraCommandsSwitch.isChecked, extraCommandsAudio.isChecked, extraCommandsVideo.isChecked) commandTemplateViewModel.insert(t) @@ -304,7 +316,6 @@ object UiUtil { item.useAsExtraCommand = extraCommandsSwitch.isChecked item.useAsExtraCommandAudio = extraCommandsAudio.isChecked item.useAsExtraCommandVideo = extraCommandsVideo.isChecked - Log.e("aa", item.toString()) commandTemplateViewModel.update(item) newTemplate(item) } @@ -1037,7 +1048,7 @@ object UiUtil { } - suspend fun showCommandTemplates(activity: Activity, commandTemplateViewModel: CommandTemplateViewModel, itemSelected: (itemSelected: List) -> Unit) { + suspend fun showCommandTemplates(activity: Activity, commandTemplateViewModel: CommandTemplateViewModel, onlyOne: Boolean = false, itemSelected: (itemSelected: List) -> Unit) { val bottomSheet = BottomSheetDialog(activity) bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) bottomSheet.setContentView(R.layout.command_template_list) @@ -1050,6 +1061,7 @@ object UiUtil { linearLayout!!.removeAllViews() val selectedItems = mutableListOf() val ok = bottomSheet.findViewById(R.id.command_ok) + ok?.isVisible = !onlyOne ok?.isEnabled = list.size == 1 list.forEach {template -> @@ -1065,6 +1077,11 @@ object UiUtil { (it as MaterialCardView).isChecked = true } + if (onlyOne) { + itemSelected(listOf(template)) + bottomSheet.cancel() + } + ok?.isEnabled = selectedItems.isNotEmpty() } linearLayout.addView(item) diff --git a/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt index f1a5f627..a34d759a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt @@ -21,6 +21,7 @@ import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.util.Extensions.getIntByAny import com.deniscerri.ytdl.util.Extensions.getStringByAny 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.FileUtil import com.deniscerri.ytdl.util.FormatUtil @@ -51,7 +52,14 @@ class YTDLPUtil(private val context: Context) { val request : YoutubeDLRequest 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{ request = YoutubeDLRequest(emptyList()) when (searchEngine){ @@ -64,13 +72,8 @@ class YTDLPUtil(private val context: Context) { } } } - val lang = sharedPreferences.getString("app_language", "en") if (searchEngine == "ytsearch" || query.isYoutubeURL()) { - var extractorArgs = "player_client=default,mediaconnect" - if (context.getStringArray(R.array.subtitle_langs).contains(lang)) { - extractorArgs += ";lang=$lang" - } - request.addOption("--extractor-args", "youtube:$extractorArgs") + request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}") } request.addOption("--flat-playlist") @@ -103,7 +106,7 @@ class YTDLPUtil(private val context: Context) { request.addOption("-P", FileUtil.getCachePath(context) + "/tmp") - + println(parseYTDLRequestString(request)) val youtubeDLResponse = YoutubeDL.getInstance().execute(request) val results: List = try { 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") 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() if (duration != "-1"){ duration = jsonObject.getInt("duration").toStringDuration(Locale.US) @@ -689,12 +692,7 @@ class YTDLPUtil(private val context: Context) { } if (downloadItem.url.isYoutubeURL()) { - 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" - } - request.addOption("--extractor-args", "youtube:$extractorArgs") + request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}") } } @@ -1098,4 +1096,18 @@ class YTDLPUtil(private val context: Context) { } 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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/util/extractors/newpipe/NewPipeUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/extractors/newpipe/NewPipeUtil.kt index f26f2d47..f0a93f36 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/extractors/newpipe/NewPipeUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/extractors/newpipe/NewPipeUtil.kt @@ -151,6 +151,8 @@ class NewPipeUtil(context: Context) { fun getChannelData(url: String, progress: (pagedResults: MutableList) -> Unit) : Result> { try { + //TODO BROKEN FOR NOW + return Result.failure(Throwable()) val req = ChannelInfo.getInfo(ServiceList.YouTube, url) println(Gson().toJson(req)) val items = mutableListOf() @@ -326,7 +328,7 @@ class NewPipeUtil(context: Context) { if (stream.audioStreams.isNotEmpty()){ for (f in 0 until stream.audioStreams.size){ val it = stream.audioStreams[f] - if (it.bitrate == 0) continue + if (it.bitrate == 0 || listOf(599, 600).contains(it.itag)) continue val formatObj = Format( format_id = it.itag.toString(), diff --git a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt index 3d288fdf..32569951 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt @@ -18,7 +18,6 @@ import androidx.work.ForegroundInfo import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkerParameters -import androidx.work.await import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdl.App import com.deniscerri.ytdl.MainActivity @@ -38,6 +37,7 @@ import com.yausername.youtubedl_android.YoutubeDL import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -53,6 +53,13 @@ class DownloadWorker( @SuppressLint("RestrictedApi") override suspend fun doWork(): Result { 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 ytdlpUtil = YTDLPUtil(context) @@ -66,10 +73,11 @@ class DownloadWorker( val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val time = System.currentTimeMillis() + 6000 val queuedItems = dao.getQueuedScheduledDownloadsUntil(time) - val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await() - if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success() + val priorityItemIDs = inputData.getLongArray("priority_item_ids")!!.toMutableList() + val continueAfterPriorityIds = inputData.getBoolean("continue_after_priority_ids", true) // this is needed for observe sources call, so it wont create result items + // [removed] //val createResultItem = inputData.getBoolean("createResultItem", true) val confTmp = Configuration(context.resources.configuration) @@ -100,8 +108,9 @@ class DownloadWorker( val foregroundInfo = ForegroundInfo(1000000000, workNotif) setForegroundAsync(foregroundInfo) - queuedItems.collect { items -> - if (isStopped) return@collect + queuedItems.collectLatest { items -> + if (this@DownloadWorker.isStopped) return@collectLatest + runningYTDLInstances.clear() val activeDownloads = dao.getActiveDownloadsList() activeDownloads.forEach { @@ -112,17 +121,28 @@ class DownloadWorker( val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) if (items.isEmpty() && running.isEmpty()) { WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) - return@collect + return@collectLatest } if (useScheduler){ if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) { 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 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 -> val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url }) @@ -365,7 +385,10 @@ class DownloadWorker( } 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) } } diff --git a/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt index f08bf103..e22836d1 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt @@ -180,7 +180,7 @@ class ObserveSourceWorker( } if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || queuedItems.any { it.downloadStartTime > 0L } ){ - downloadRepo.startDownloadWorker(queuedItems, context, Data.Builder().putBoolean("createResultItem", false)) + downloadRepo.startDownloadWorker(queuedItems, context) if(!useScheduler){ queuedItems.filter { it.downloadStartTime != 0L || (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach { diff --git a/app/src/main/res/drawable/baseline_exit_to_app_24.xml b/app/src/main/res/drawable/baseline_exit_to_app_24.xml new file mode 100644 index 00000000..861c3f2a --- /dev/null +++ b/app/src/main/res/drawable/baseline_exit_to_app_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout-sw600dp/fragment_home.xml b/app/src/main/res/layout-sw600dp/fragment_home.xml index 0e1a6ec6..5cdf1414 100644 --- a/app/src/main/res/layout-sw600dp/fragment_home.xml +++ b/app/src/main/res/layout-sw600dp/fragment_home.xml @@ -128,13 +128,14 @@ diff --git a/app/src/main/res/layout/active_download_card.xml b/app/src/main/res/layout/active_download_card.xml index a6adb90c..7a7c08d7 100644 --- a/app/src/main/res/layout/active_download_card.xml +++ b/app/src/main/res/layout/active_download_card.xml @@ -166,21 +166,21 @@ app:layout_constraintVertical_bias="0.51" > - - - - - - - - - - - - - - - + - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/content" + app:layout_constraintHorizontal_bias="0.0" + android:layout_height="wrap_content"> + + + + + + + + + + + + diff --git a/app/src/main/res/layout/create_command_template.xml b/app/src/main/res/layout/create_command_template.xml index 4b6f1856..c96001e1 100644 --- a/app/src/main/res/layout/create_command_template.xml +++ b/app/src/main/res/layout/create_command_template.xml @@ -102,54 +102,60 @@ + - + + + android:layout_marginHorizontal="10dp" + android:orientation="vertical"> + + + android:orientation="horizontal"> - - - + android:layout_marginEnd="20dp" + android:visibility="gone" + android:checked="true" + android:text="@string/audio" /> - - - - - + - + + android:layout_marginTop="20dp" + android:orientation="horizontal"> @@ -29,9 +29,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" - android:text="@string/resume" + android:text="@string/resume_all" android:layout_margin="16dp" - android:contentDescription="@string/resume" + android:contentDescription="@string/resume_all" app:icon="@drawable/exomedia_ic_play_arrow_white" app:useCompatPadding="true" /> diff --git a/app/src/main/res/layout/fragment_command_templates.xml b/app/src/main/res/layout/fragment_command_templates.xml index e448b498..0572ef77 100644 --- a/app/src/main/res/layout/fragment_command_templates.xml +++ b/app/src/main/res/layout/fragment_command_templates.xml @@ -94,7 +94,6 @@ - diff --git a/app/src/main/res/layout/fragment_history.xml b/app/src/main/res/layout/fragment_history.xml index 4b66418d..c8150ee6 100644 --- a/app/src/main/res/layout/fragment_history.xml +++ b/app/src/main/res/layout/fragment_history.xml @@ -37,7 +37,6 @@ - - - - - - - - - - - diff --git a/app/src/main/res/layout/history_other_filters_sheet.xml b/app/src/main/res/layout/history_other_filters_sheet.xml new file mode 100644 index 00000000..1b366ba8 --- /dev/null +++ b/app/src/main/res/layout/history_other_filters_sheet.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/select_playlist_items.xml b/app/src/main/res/layout/select_playlist_items.xml index 8e00e409..9223fd67 100644 --- a/app/src/main/res/layout/select_playlist_items.xml +++ b/app/src/main/res/layout/select_playlist_items.xml @@ -40,6 +40,7 @@ @@ -57,6 +58,7 @@ diff --git a/app/src/main/res/menu/active_downloads_minified.xml b/app/src/main/res/menu/active_downloads_minified.xml index 427dedcb..bf150d99 100644 --- a/app/src/main/res/menu/active_downloads_minified.xml +++ b/app/src/main/res/menu/active_downloads_minified.xml @@ -3,19 +3,19 @@ xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity" > - - - - - - + + - - - - - - + + + + Use AlarmManager instead of WorkManager for Scheduling Enable this if WorkManager is restricted by your device vendor or scheduled jobs are not too precise Use Item URL instead of Playlist URL - Useful for Libretube offline playlists that are not recognised by yt-dlp. Playlist metadata wont be embedded with this enabled + Playlist metadata wont be embedded with this enabled Recode Video Recodes the video file to the specified video format Daily @@ -422,4 +422,11 @@ Use Playlist Name as Album Metadata If Album Metadata is not present, use playlist name instead Data Fetching Extractor (YouTube) + Pause All + Resume All + Package Name + Preferred Command Template + Status + Deleted + Websites \ No newline at end of file diff --git a/app/src/main/res/xml/general_preferences.xml b/app/src/main/res/xml/general_preferences.xml index d151d40c..846f9c9d 100644 --- a/app/src/main/res/xml/general_preferences.xml +++ b/app/src/main/res/xml/general_preferences.xml @@ -102,6 +102,13 @@ app:useSimpleSummaryProvider="true" app:title="@string/format_source" /> + + +