From d62dc7f9dc15b50f5a9bb49d7b951e46a7ed2e4d Mon Sep 17 00:00:00 2001 From: zaednasr <75589932+zaednasr@users.noreply.github.com> Date: Sat, 23 Mar 2024 10:34:47 +0100 Subject: [PATCH] tmp commit --- app/build.gradle | 18 +- app/src/main/AndroidManifest.xml | 160 ++++++- .../main/java/com/deniscerri/ytdlnis/App.kt | 2 +- .../com/deniscerri/ytdlnis/MainActivity.kt | 36 +- .../ytdlnis/database/dao/DownloadDao.kt | 5 +- .../ytdlnis/database/dao/ResultDao.kt | 5 +- .../ytdlnis/database/dao/TerminalDao.kt | 4 +- .../database/repository/HistoryRepository.kt | 20 + .../database/repository/ResultRepository.kt | 2 +- .../viewmodel/CommandTemplateViewModel.kt | 11 + .../database/viewmodel/CookieViewModel.kt | 86 ++-- .../database/viewmodel/DownloadViewModel.kt | 14 +- .../database/viewmodel/HistoryViewModel.kt | 11 + .../database/viewmodel/ResultViewModel.kt | 71 +-- .../com/deniscerri/ytdlnis/ui/HomeFragment.kt | 98 ++-- .../ui/adapter/AlreadyExistsAdapter.kt | 92 +++- .../ytdlnis/ui/adapter/HistoryAdapter.kt | 7 + .../ytdlnis/ui/adapter/HomeAdapter.kt | 33 +- .../ytdlnis/ui/adapter/PlaylistAdapter.kt | 5 +- .../ytdlnis/ui/adapter/TemplatesAdapter.kt | 8 + .../ConfigureDownloadBottomSheetDialog.kt | 6 +- .../downloadcard/CutVideoBottomSheetDialog.kt | 14 +- .../downloadcard/DownloadBottomSheetDialog.kt | 8 +- .../ui/downloadcard/DownloadVideoFragment.kt | 2 +- .../FormatSelectionBottomSheetDialog.kt | 3 +- .../downloadcard/SelectPlaylistItemsDialog.kt | 137 +++--- .../downloads/CancelledDownloadsFragment.kt | 1 + .../ui/downloads/ErroredDownloadsFragment.kt | 3 +- .../ytdlnis/ui/downloads/HistoryFragment.kt | 72 +-- .../ui/downloads/QueuedDownloadsFragment.kt | 3 +- .../ui/downloads/SavedDownloadsFragment.kt | 3 +- .../ui/more/CommandTemplatesFragment.kt | 72 ++- .../ytdlnis/ui/more/CookiesFragment.kt | 5 +- .../ytdlnis/ui/more/WebViewActivity.kt | 33 +- .../downloadLogs/DownloadLogListFragment.kt | 30 +- .../more/settings/FolderSettingsFragment.kt | 18 +- .../deniscerri/ytdlnis/util/CrashListener.kt | 46 ++ .../com/deniscerri/ytdlnis/util/Extensions.kt | 72 ++- .../com/deniscerri/ytdlnis/util/FileUtil.kt | 27 +- .../com/deniscerri/ytdlnis/util/InfoUtil.kt | 94 ++-- .../ytdlnis/util/NotificationUtil.kt | 15 +- .../com/deniscerri/ytdlnis/util/UiUtil.kt | 16 +- .../com/deniscerri/ytdlnis/util/UpdateUtil.kt | 2 +- .../deniscerri/ytdlnis/work/DownloadWorker.kt | 9 +- .../ytdlnis/work/MoveCacheFilesWorker.kt | 6 +- .../ytdlnis/work/TerminalDownloadWorker.kt | 7 +- .../main/res/layout/already_exists_card.xml | 16 - .../main/res/layout/already_exists_item.xml | 252 ++++++++-- .../layout/download_multiple_bottom_sheet.xml | 1 + app/src/main/res/layout/format_item.xml | 2 +- .../main/res/layout/format_item_shimmer.xml | 4 +- app/src/main/res/layout/fragment_history.xml | 28 +- app/src/main/res/layout/playlist_item.xml | 186 ++++---- .../main/res/layout/select_playlist_items.xml | 97 ++-- .../main/res/menu/history_menu_context.xml | 7 + app/src/main/res/menu/main_menu_context.xml | 7 + .../main/res/menu/select_playlist_menu.xml | 7 + .../main/res/menu/templates_menu_context.xml | 7 + .../attrs.xml | 0 app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/folders_preference.xml | 9 + .../main/res/xml/processing_preferences.xml | 438 +++++++++--------- app/src/main/res/xml/updating_preferences.xml | 2 +- build.gradle | 4 +- 64 files changed, 1624 insertions(+), 837 deletions(-) create mode 100644 app/src/main/java/com/deniscerri/ytdlnis/util/CrashListener.kt delete mode 100644 app/src/main/res/layout/already_exists_card.xml rename app/src/main/res/{values-sw800dp-land => values-sw600dp-land}/attrs.xml (100%) diff --git a/app/build.gradle b/app/build.gradle index 4db043aa..d38a1712 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,13 +10,13 @@ plugins { def properties = new Properties() def versionMajor = 1 def versionMinor = 7 -def versionPatch = 3 -def versionBuild = 1 // bump for dogfood builds, public betas, etc. +def versionPatch = 4 +def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionExt = ".${versionBuild}" -//if (versionBuild > 0){ -// versionExt = ".${versionBuild}-beta" -//} +if (versionBuild > 0){ + versionExt = ".${versionBuild}-beta" +} android { namespace 'com.deniscerri.ytdlnis' @@ -144,7 +144,7 @@ dependencies { implementation "androidx.navigation:navigation-ui-ktx:$navVer" implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.test.ext:junit-ktx:1.1.5' - implementation 'androidx.compose.ui:ui-android:1.5.4' + implementation 'androidx.compose.ui:ui-android:1.6.2' testImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer" androidTestImplementation "androidx.test.ext:junit:$androidJunitVer" @@ -168,7 +168,7 @@ dependencies { implementation 'androidx.paging:paging-runtime-ktx:3.2.1' implementation "androidx.room:room-paging:$roomVer" androidTestImplementation "androidx.room:room-testing:$roomVer" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation "androidx.compose.runtime:runtime:$composeVer" androidTestImplementation 'com.google.truth:truth:1.1.5' @@ -180,7 +180,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.6.2" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0" implementation "com.github.Irineu333:Highlight-KT:1.0.4" // For media playback using ExoPlayer @@ -196,6 +196,6 @@ 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.0-beta01' + implementation 'androidx.compose.material3:material3-android:1.2.0' implementation "io.noties.markwon:core:4.6.2" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1257f698..2f02a4bb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,19 +4,23 @@ - + - - + - + @@ -71,6 +75,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - if (navigationView is NavigationBarView){ - when(destination.id){ - R.id.homeFragment, R.id.historyFragment, R.id.moreFragment -> showBottomNavigation() - else -> hideBottomNavigation() + Handler(Looper.getMainLooper()).post { + if (navigationView is NavigationBarView){ + when(destination.id){ + R.id.homeFragment, R.id.historyFragment, R.id.moreFragment -> showBottomNavigation() + else -> hideBottomNavigation() + } } - } + } navigationView.visibilityChanged { @@ -288,9 +299,16 @@ class MainActivity : BaseActivity() { findViewById(R.id.frame_layout).updateLayoutParams { this.width = LayoutParams.MATCH_PARENT } - navigationView.animate()?.translationX(-navigationView.width.toFloat())?.setDuration(300)?.withEndAction { - navigationView.visibility = View.GONE - }?.start() + + if (resources.getBoolean(R.bool.is_right_to_left)){ + navigationView.animate()?.translationX(navigationView.width.toFloat())?.setDuration(300)?.withEndAction { + navigationView.visibility = View.GONE + }?.start() + }else{ + navigationView.animate()?.translationX(-navigationView.width.toFloat())?.setDuration(300)?.withEndAction { + navigationView.visibility = View.GONE + }?.start() + } } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt index 9bd915b7..313329f3 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt @@ -59,7 +59,7 @@ interface DownloadDao { fun getProcessingDownloadsList() : List @Query("SELECT * FROM downloads WHERE status='Active'") - fun getActiveDownloadsList() : List + suspend fun getActiveDownloadsList() : List @Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused','PausedReQueued')") fun getActiveAndQueuedDownloadsList() : List @@ -250,4 +250,7 @@ interface DownloadDao { @Query("SELECT id from downloads WHERE id > :item1 AND id < :item2 AND status in (:statuses) ORDER BY id DESC") fun getIDsBetweenTwoItems(item1: Long, item2: Long, statuses: List) : List + + @Query("SELECT id from downloads WHERE id > :item1 AND id < :item2 AND status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id") + fun getQueuedIDsBetweenTwoItems(item1: Long, item2: Long) : List } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/ResultDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/ResultDao.kt index 7726d340..cc0728bd 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/ResultDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/ResultDao.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.Flow @Dao interface ResultDao { - @Query("SELECT * FROM results") + @Query("SELECT * FROM results order by id") fun getResults() : Flow> @Query("SELECT COUNT(id) FROM results") @@ -49,4 +49,7 @@ interface ResultDao { @Query("SELECT * FROM results WHERE url=:url LIMIT 1") fun getResultByURL(url: String) : ResultItem? + @Query("SELECT * from results WHERE id > :item1 AND id < :item2 ORDER BY id") + fun getResultsBetweenTwoItems(item1: Long, item2: Long) : List + } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/TerminalDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/TerminalDao.kt index f1592553..945fc29a 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/TerminalDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/TerminalDao.kt @@ -24,12 +24,12 @@ interface TerminalDao { fun getActiveTerminalsCount() : Int @Query("UPDATE terminalDownloads set log=:l where id=:id") - fun updateTerminalLog(l: String, id: Long) + suspend fun updateTerminalLog(l: String, id: Long) @Query("SELECT * FROM terminalDownloads WHERE id=:id LIMIT 1") fun getTerminalById(id: Long) : TerminalItem? @Transaction - fun updateLog(line: String, id: Long){ + suspend fun updateLog(line: String, id: Long){ val t = getTerminalById(id) ?: return val log = t.log ?: "" updateTerminalLog(log.appendLineToLog(line), id) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt index 9f13a414..21afdb7c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt @@ -46,6 +46,26 @@ class HistoryRepository(private val historyDao: HistoryDao) { 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 } + } + } + } + if(notDeleted){ + filtered = filtered.filter { it.downloadPath.any { it2 -> FileUtil.exists(it2) } } + } + + return filtered + } + suspend fun insert(item: HistoryItem){ historyDao.insert(item) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt index 0368e712..7294969e 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt @@ -145,7 +145,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co search(inputQuery, resetResults, addToResults) } SourceType.YT_DLP -> { - getDefault(inputQuery, resetResults, addToResults) + getDefault(inputQuery, resetResults, addToResults, singleItem) } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CommandTemplateViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CommandTemplateViewModel.kt index 74c6546e..c597d191 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CommandTemplateViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CommandTemplateViewModel.kt @@ -179,4 +179,15 @@ class CommandTemplateViewModel(private val application: Application) : AndroidVi } } + fun getRecordsBetweenTwoItems(item1: Long, item2: Long) : List { + val filtered = repository.getFiltered(queryFilter.value!!, sortType.value!!, sortOrder.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 } + }else{ + return filtered.filterIndexed { index, _ -> index in (firstIndex + 1) until secondIndex } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt index aac001af..0e7c902d 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt @@ -1,11 +1,15 @@ package com.deniscerri.ytdlnis.database.viewmodel +import android.R.attr.description +import android.R.attr.duration import android.annotation.SuppressLint import android.app.Application import android.content.ClipboardManager +import android.content.ContentValues import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase.OPEN_READONLY +import android.database.sqlite.SQLiteDatabase.OPEN_READWRITE import android.os.Environment import android.util.Log import android.webkit.CookieManager @@ -18,12 +22,14 @@ import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.repository.CookieRepository import com.deniscerri.ytdlnis.ui.more.WebViewActivity +import com.deniscerri.ytdlnis.util.Extensions.convertNetscapeToSetCookie +import com.deniscerri.ytdlnis.util.FileUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.io.File -import java.net.URL import java.util.Date + class CookieViewModel(private val application: Application) : AndroidViewModel(application) { private val repository: CookieRepository val items: LiveData> @@ -33,6 +39,15 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a "# WebView Generated by the YTDLnis app\n" + "# This is a generated file! Do not edit." + private val projection = arrayOf( + CookieObject.HOST, + CookieObject.EXPIRY, + CookieObject.PATH, + CookieObject.NAME, + CookieObject.VALUE, + CookieObject.SECURE + ) + init { val dao = DBManager.getInstance(application).cookieDao repository = CookieRepository(dao) @@ -86,14 +101,7 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a dbPath.absolutePath, null, OPEN_READONLY ) - val projection = arrayOf( - CookieObject.HOST, - CookieObject.EXPIRY, - CookieObject.PATH, - CookieObject.NAME, - CookieObject.VALUE, - CookieObject.SECURE - ) + val cookieList = mutableListOf() db.query( "cookies", projection, null, null, null, null, null @@ -108,20 +116,16 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a val host = if (hostKey[0] != '.') ".$hostKey" else hostKey - - val requiredHost = URL(url).host - if(host.contains(requiredHost)){ - cookieList.add( - WebViewActivity.CookieItem( - domain = host, - name = name, - value = value, - path = path, - secure = secure, - expiry = expiry - ) + cookieList.add( + WebViewActivity.CookieItem( + domain = host, + name = name, + value = value, + path = path, + secure = secure, + expiry = expiry ) - } + ) } close() } @@ -134,14 +138,17 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a fun updateCookiesFile() = viewModelScope.launch(Dispatchers.IO) { val cookies = repository.getAll() val cookieTXT = StringBuilder(cookieHeader) - val cookieFile = File(application.cacheDir, "cookies.txt") - if (cookies.isEmpty()) cookieFile.apply { writeText("") } - cookies.forEach { - it.content.lines().forEach {line -> - if (! cookieTXT.contains(line)) cookieTXT.append(it.content) + FileUtil.getCookieFile(application, true){ c -> + val cookieFile = File(c) + if (cookies.isEmpty()) cookieFile.apply { writeText("") } + cookies.forEach { + it.content.lines().forEach {line -> + if (! cookieTXT.contains(line)) cookieTXT.append(it.content) + } } + cookieFile.apply { writeText(cookieTXT.toString()) } } - cookieFile.apply { writeText(cookieTXT.toString()) } + } suspend fun importFromClipboard() { @@ -170,11 +177,14 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a try{ val clipboard: ClipboardManager = application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val cookieFile = File(application.cacheDir, "cookies.txt") - if (! cookieFile.exists()) updateCookiesFile() - cookieFile.readText().let { - clipboard.setText(it) + FileUtil.getCookieFile(application, true){c -> + val cookieFile = File(c) + if (! cookieFile.exists()) updateCookiesFile() + cookieFile.readText().let { + clipboard.setText(it) + } } + }catch (e: Exception){ e.printStackTrace() } @@ -182,12 +192,14 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a fun exportToFile(exported: (File?) -> Unit) { try{ - val cookieFile = File(application.cacheDir, "cookies.txt") - if (!cookieFile.exists()) updateCookiesFile() + FileUtil.getCookieFile(application, true){ c -> + val cookieFile = File(c) + if (!cookieFile.exists()) updateCookiesFile() - val downloads = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "YTDLnis_Cookies.txt") - val file = cookieFile.copyTo(downloads, true) - exported(file) + val downloads = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "YTDLnis_Cookies.txt") + val file = cookieFile.copyTo(downloads, true) + exported(file) + } }catch (e: Exception){ e.printStackTrace() exported(null) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt index 8a640305..64461768 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt @@ -1,5 +1,6 @@ package com.deniscerri.ytdlnis.database.viewmodel +import android.annotation.SuppressLint import android.app.Application import android.content.SharedPreferences import android.content.res.Configuration @@ -15,6 +16,7 @@ import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager +import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.DBManager @@ -179,10 +181,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel if (sharedPreferences.getBoolean("incognito", false)){ if (item.status == DownloadRepository.Status.Cancelled.toString() || item.status == DownloadRepository.Status.Error.toString()){ repository.delete(item.id) + return } - }else{ - repository.update(item) } + + repository.update(item) } fun getItemByID(id: Long) : DownloadItem { @@ -461,7 +464,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel sharedPreferences.getString("audio_language", "")?.apply { if (this.isNotBlank()){ - requirements.add { it: Format -> it.lang == this } + requirements.add { it: Format -> it.lang?.contains(this) == true } } } @@ -476,6 +479,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel requirements.add { it: Format -> if (videoContainer == "mp4") it.container.equals("mpeg_4", true) else it.container.equals(videoContainer, true)} requirements.add { it: Format -> it.format_note.contains(videoQualityPreference.split("_")[0].dropLast(1)) } requirements.add { it: Format -> "^(${videoCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)} + requirements.add { it: Format -> it.acodec == "none" || it.acodec == "" } return requirements } @@ -948,5 +952,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel return dao.getIDsBetweenTwoItems(item1, item2, statuses) } + fun getQueuedIDsBetweenTwoItems(item1: Long, item2: Long) : List { + return dao.getQueuedIDsBetweenTwoItems(item1, item2) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/HistoryViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/HistoryViewModel.kt index 93adf859..8f8a9d7b 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/HistoryViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/HistoryViewModel.kt @@ -117,4 +117,15 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application) repository.clearDeletedHistory() } + fun getRecordsBetweenTwoItems(item1: Long, item2: Long) : List { + val filtered = repository.getFiltered(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.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 } + }else{ + return filtered.filterIndexed { index, _ -> index in (firstIndex + 1) until secondIndex } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt index 61c8eac6..6d8d034f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt @@ -13,6 +13,8 @@ import androidx.preference.PreferenceManager import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.DBManager +import com.deniscerri.ytdlnis.database.dao.DownloadDao +import com.deniscerri.ytdlnis.database.dao.ResultDao import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.SearchHistoryItem @@ -37,6 +39,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a val items : LiveData> private val infoUtil: InfoUtil private val notificationUtil: NotificationUtil + private val dao: ResultDao data class ResultsUiState( var processing: Boolean, var errorMessage: Pair?, @@ -67,7 +70,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a private val sharedPreferences: SharedPreferences init { - val dao = DBManager.getInstance(application).resultDao + dao = DBManager.getInstance(application).resultDao repository = ResultRepository(dao, getApplication().applicationContext) searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao) items = repository.allResults.asLiveData() @@ -156,7 +159,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a } } - fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { + suspend fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { repository.deleteAll() } @@ -219,7 +222,8 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a viewModelScope.launch(Dispatchers.IO) { updatingData.emit(false) updatingData.value = false - updateResultData.emit(mutableListOf()) + updateResultData.emit(null) + updateResultData.value = null } } } @@ -231,53 +235,66 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a updatingData.emit(false) updatingData.value = false updateResultData.emit(null) + updateResultData.value = null } suspend fun cancelUpdateFormatsItemData(){ - updateFormatsResultDataJob?.cancel(CancellationException()) updatingFormats.emit(false) + updatingFormats.value = false updateFormatsResultData.emit(null) + updateFormatsResultData.value = null + updateFormatsResultDataJob?.cancel(CancellationException()) } suspend fun updateFormatItemData(result: ResultItem){ if (updateFormatsResultDataJob == null || updateFormatsResultDataJob?.isCancelled == true || updateFormatsResultDataJob?.isCompleted == true) { updateFormatsResultDataJob = viewModelScope.launch(Dispatchers.IO) { updatingFormats.emit(true) - val formats = kotlin.runCatching { - infoUtil.getFormats(result.url) - }.onFailure { e -> - if (e !is kotlinx.coroutines.CancellationException){ - uiState.update {it.copy( - processing = false, - errorMessage = Pair(R.string.no_results, e.message.toString()), - actions = mutableListOf(Pair(R.string.copy_log, ResultAction.COPY_LOG)) - )} + try { + val formats = infoUtil.getFormats(result.url) + updatingFormats.emit(false) + formats.apply { + if (formats.isNotEmpty() && updateFormatsResultDataJob?.isCancelled == false) { + getItemByURL(result.url)?.apply { + this.formats = formats.toMutableList() + update(this) + } + updateFormatsResultData.emit(formats.toMutableList()) + } + } + }catch(e: Exception) { + if (e !is kotlinx.coroutines.CancellationException) { + uiState.update { + it.copy( + processing = false, + errorMessage = Pair(R.string.no_results, e.message.toString()), + actions = mutableListOf( + Pair( + R.string.copy_log, + ResultAction.COPY_LOG + ) + ) + ) + } Log.e(tag, e.toString()) } - }.getOrNull() - - updatingFormats.emit(false) - - formats?.apply { - if (formats.isNotEmpty() && isActive) { - getItemByURL(result.url)?.apply { - this.formats = formats.toMutableList() - update(this) - } - updateFormatsResultData.emit(formats.toMutableList()) - } } - } updateFormatsResultDataJob?.start() updateFormatsResultDataJob?.invokeOnCompletion { if (it != null){ viewModelScope.launch(Dispatchers.IO) { updatingFormats.emit(false) - updateFormatsResultData.emit(mutableListOf()) + updatingFormats.value = false + updateFormatsResultData.emit(null) + updateFormatsResultData.value = null } } } } } + + fun getResultsBetweenTwoItems(item1: Long, item2: Long) : List{ + return dao.getResultsBetweenTwoItems(item1, item2) + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt index 8ab63b6a..77af5037 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt @@ -37,12 +37,14 @@ import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.SearchSuggestionItem import com.deniscerri.ytdlnis.database.models.SearchSuggestionType +import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.ui.adapter.HomeAdapter import com.deniscerri.ytdlnis.ui.adapter.SearchSuggestionsAdapter import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll +import com.deniscerri.ytdlnis.util.Extensions.toListString import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.ThemeUtil import com.deniscerri.ytdlnis.util.UiUtil @@ -99,7 +101,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti private var searchSuggestionsRecyclerView: RecyclerView? = null private var uiHandler: Handler? = null private var resultsList: List? = null - private var selectedObjects: ArrayList? = null + private lateinit var selectedObjects: ArrayList private var quickLaunchSheet = false private var sharedPreferences: SharedPreferences? = null private var actionMode: ActionMode? = null @@ -115,6 +117,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti mainActivity = activity as MainActivity? quickLaunchSheet = false infoUtil = InfoUtil(requireContext()) + selectedObjects = arrayListOf() return fragmentView } @@ -136,7 +139,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) //initViews - shimmerCards = view.findViewById(R.id.shimmer_results_framelayout) searchBar = view.findViewById(R.id.search_bar) searchView = view.findViewById(R.id.search_view) appBarLayout = view.findViewById(R.id.home_appbarlayout) @@ -160,6 +162,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti recyclerView?.adapter = homeAdapter recyclerView?.enableFastScroll() + shimmerCards = view.findViewById(R.id.shimmer_results_framelayout) + + searchSuggestionsAdapter = SearchSuggestionsAdapter( this, @@ -214,8 +219,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti } if (inputQueries != null) { - resultViewModel.deleteAll() lifecycleScope.launch(Dispatchers.IO){ + resultViewModel.deleteAll() resultViewModel.parseQueries(inputQueries!!){} inputQueries = null } @@ -606,9 +611,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti resultViewModel.addSearchQueryToHistory(q) } } - resultViewModel.deleteAll() lifecycleScope.launch(Dispatchers.IO){ - Thread.sleep(300) + resultViewModel.deleteAll() if(sharedPreferences!!.getBoolean("quick_download", false) || sharedPreferences!!.getString("preferred_download_type", "video") == "command"){ if (queryList.size == 1 && Patterns.WEB_URL.matcher(queryList.first()).matches()){ if (sharedPreferences!!.getBoolean("download_card", true)) { @@ -683,23 +687,28 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti } override fun onCardClick(videoURL: String, add: Boolean) { - val item = resultsList?.find { it?.url == videoURL } - if (add) { - selectedObjects!!.add(item!!) - if (actionMode == null){ - actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) - }else{ - actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" - } - } else { - selectedObjects!!.remove(item) - actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" - if (selectedObjects!!.isEmpty()){ - actionMode?.finish() + lifecycleScope.launch { + val item = resultsList?.find { it?.url == videoURL } + if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) + actionMode?.apply { + if (add) selectedObjects.add(item!!) + else selectedObjects.remove(item!!) + + if (selectedObjects.size == 0){ + this.finish() + }else{ + actionMode?.title = "${selectedObjects.size} ${getString(R.string.selected)}" + this.menu.findItem(R.id.select_between).isVisible = false + if(selectedObjects.size == 2){ + val selectedIDs = selectedObjects.sortedBy { it.id } + val resultsInMiddle = withContext(Dispatchers.IO){ + resultViewModel.getResultsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id) + }.toMutableList() + this.menu.findItem(R.id.select_between).isVisible = resultsInMiddle.isNotEmpty() + } + } } } - - } override fun onCardDetailsClick(videoURL: String) { @@ -739,7 +748,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti private val contextualActionBar = object : ActionMode.Callback { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { mode!!.menuInflater.inflate(R.menu.main_menu_context, menu) - mode.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" + mode.title = "${selectedObjects.size} ${getString(R.string.selected)}" searchBar!!.isEnabled = false searchBar!!.menu.forEach { it.isEnabled = false } (activity as MainActivity).disableBottomNavigation() @@ -758,15 +767,32 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti item: MenuItem? ): Boolean { return when (item!!.itemId) { + R.id.select_between -> { + lifecycleScope.launch { + val selectedIDs = selectedObjects.sortedBy { it.id } + val resultsInMiddle = withContext(Dispatchers.IO){ + resultViewModel.getResultsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id) + }.toMutableList() + if (resultsInMiddle.isNotEmpty()){ + selectedObjects.addAll(resultsInMiddle) + homeAdapter?.checkMultipleItems(selectedObjects.map { it.url }) + actionMode?.title = "${selectedObjects.count()} ${getString(R.string.selected)}" + } + mode?.menu?.findItem(R.id.select_between)?.isVisible = false + } + true + } R.id.delete_results -> { val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!) deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> - if (selectedObjects?.size == resultsList?.size){ - resultViewModel.deleteAll() + if (selectedObjects.size == resultsList?.size){ + lifecycleScope.launch(Dispatchers.IO){ + resultViewModel.deleteAll() + } }else{ - resultViewModel.deleteSelected(selectedObjects!!.toList()) + resultViewModel.deleteSelected(selectedObjects.toList()) } clearCheckedItems() actionMode?.finish() @@ -776,14 +802,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti } R.id.download -> { lifecycleScope.launch { - if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects!!.size == 1) { + if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects.size == 1) { showSingleDownloadSheet( - selectedObjects!![0], - downloadViewModel.getDownloadType(url = selectedObjects!![0].url) + selectedObjects[0], + downloadViewModel.getDownloadType(url = selectedObjects[0].url) ) }else{ val downloadList = withContext(Dispatchers.IO){ - downloadViewModel.turnResultItemsToDownloadItems(selectedObjects!!) + downloadViewModel.turnResultItemsToDownloadItems(selectedObjects) } if (sharedPreferences!!.getBoolean("download_card", true)) { @@ -803,8 +829,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti } R.id.select_all -> { homeAdapter?.checkAll(resultsList) - selectedObjects?.clear() - resultsList?.forEach { selectedObjects?.add(it!!) } + selectedObjects.clear() + resultsList?.forEach { selectedObjects.add(it!!) } mode?.title = getString(R.string.all_items_selected) true } @@ -812,11 +838,11 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti homeAdapter?.invertSelected(resultsList) val invertedList = arrayListOf() resultsList?.forEach { - if (!selectedObjects?.contains(it)!!) invertedList.add(it!!) + if (!selectedObjects.contains(it)) invertedList.add(it!!) } - selectedObjects?.clear() - selectedObjects?.addAll(invertedList) - actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" + selectedObjects.clear() + selectedObjects.addAll(invertedList) + actionMode!!.title = "${selectedObjects.size} ${getString(R.string.selected)}" if (invertedList.isEmpty()) actionMode?.finish() true } @@ -836,10 +862,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti private fun clearCheckedItems(){ homeAdapter?.clearCheckedItems() - selectedObjects?.forEach { + selectedObjects.forEach { homeAdapter?.notifyItemChanged(resultsList!!.indexOf(it)) } - selectedObjects?.clear() + selectedObjects.clear() } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/AlreadyExistsAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/AlreadyExistsAdapter.kt index adee2e73..6d8b2165 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/AlreadyExistsAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/AlreadyExistsAdapter.kt @@ -1,38 +1,46 @@ package com.deniscerri.ytdlnis.ui.adapter import android.app.Activity +import android.content.SharedPreferences +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.ImageView +import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.DownloadItem +import com.deniscerri.ytdlnis.util.Extensions.loadThumbnail import com.deniscerri.ytdlnis.util.Extensions.popup -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.deniscerri.ytdlnis.util.FileUtil +import com.google.android.material.card.MaterialCardView class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter, AlreadyExistsAdapter.ViewHolder>(AsyncDifferConfig.Builder( DIFF_CALLBACK ).build()) { private val onItemClickListener: OnItemClickListener private val activity: Activity + private val sharedPreferences: SharedPreferences init { this.onItemClickListener = onItemClickListener this.activity = activity + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) } class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val cardView: ConstraintLayout + val cardView: MaterialCardView init { - cardView = itemView as ConstraintLayout + cardView = itemView as MaterialCardView } } @@ -43,38 +51,78 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val item = getItem(position) ?: return - val alreadyExistsItem = holder.cardView - alreadyExistsItem.tag = item.first.id.toString() - alreadyExistsItem.popup() + val alreadyExistsItem = getItem(position) ?: return + val item = alreadyExistsItem.first + val historyID = alreadyExistsItem.second - val editBtn = alreadyExistsItem.findViewById