tmp commit

This commit is contained in:
zaednasr 2024-03-23 10:34:47 +01:00
parent 738ebb8558
commit d62dc7f9dc
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
64 changed files with 1624 additions and 837 deletions

View file

@ -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"
}

View file

@ -4,19 +4,23 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
android:maxSdkVersion="32"/>
<uses-permission
android:name="android.permission.SCHEDULE_EXACT_ALARM"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
@ -71,6 +75,152 @@
<data android:mimeType="video/*" />
<data android:mimeType="audio/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="youtube.com" />
<data android:host="youtube.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="twitter.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="x.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="soundcloud.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="facebook.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="instagram.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="tiktok.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="bilibili.tv" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="youtu.be" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="twitch.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="reddit.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="pinterest.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="music.youtube.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="m.youtube.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="vk.com" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="rutube.com" />
</intent-filter>
</activity>
<activity
android:name=".receiver.ShareFileActivity"

View file

@ -5,6 +5,7 @@ import android.os.Looper
import android.widget.Toast
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.util.CrashListener
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.aria2c.Aria2c
import com.yausername.ffmpeg.FFmpeg
@ -25,7 +26,6 @@ class App : Application() {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this@App)
setDefaultValues()
applicationScope = CoroutineScope(SupervisorJob())
applicationScope.launch((Dispatchers.IO)) {
try {

View file

@ -11,6 +11,8 @@ import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.view.View
@ -41,6 +43,7 @@ import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.ui.BaseActivity
import com.deniscerri.ytdlnis.ui.HomeFragment
import com.deniscerri.ytdlnis.ui.more.settings.SettingsActivity
import com.deniscerri.ytdlnis.util.CrashListener
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
@ -56,6 +59,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -81,6 +85,7 @@ class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CrashListener(this).registerExceptionHandler()
ThemeUtil.updateTheme(this)
window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this)
setContentView(R.layout.activity_main)
@ -90,7 +95,11 @@ class MainActivity : BaseActivity() {
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
preferences = PreferenceManager.getDefaultSharedPreferences(context)
if (preferences.getBoolean("incognito", false)) resultViewModel.deleteAll()
if (preferences.getBoolean("incognito", false)) {
lifecycleScope.launch(Dispatchers.IO){
resultViewModel.deleteAll()
}
}
askPermissions()
checkUpdate()
@ -211,13 +220,15 @@ class MainActivity : BaseActivity() {
}
navController.addOnDestinationChangedListener { _, destination, _ ->
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<FragmentContainerView>(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()
}
}
}

View file

@ -59,7 +59,7 @@ interface DownloadDao {
fun getProcessingDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloadsList() : List<DownloadItem>
suspend fun getActiveDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused','PausedReQueued')")
fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@ -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<String>) : List<Long>
@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<Long>
}

View file

@ -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<List<ResultItem>>
@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<ResultItem>
}

View file

@ -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)

View file

@ -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<HistoryItem> {
var filtered = when(sortType){
HistorySortType.DATE -> historyDao.getHistorySortedByID(query, format, site, sort.toString())
HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, format, site, sort.toString())
HistorySortType.AUTHOR -> historyDao.getHistorySortedByAuthor(query, format, site, sort.toString())
HistorySortType.FILESIZE -> {
val items = historyDao.getHistorySortedByID(query, format, site, sort.toString())
when(sort){
SORTING.DESC -> items.sortedByDescending { it.format.filesize }
SORTING.ASC -> items.sortedBy { it.format.filesize }
}
}
}
if(notDeleted){
filtered = filtered.filter { it.downloadPath.any { it2 -> FileUtil.exists(it2) } }
}
return filtered
}
suspend fun insert(item: HistoryItem){
historyDao.insert(item)
}

View file

@ -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)
}
}

View file

@ -179,4 +179,15 @@ class CommandTemplateViewModel(private val application: Application) : AndroidVi
}
}
fun getRecordsBetweenTwoItems(item1: Long, item2: Long) : List<CommandTemplate> {
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 }
}
}
}

View file

@ -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<List<CookieItem>>
@ -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<WebViewActivity.CookieItem>()
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)

View file

@ -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<Long> {
return dao.getQueuedIDsBetweenTwoItems(item1, item2)
}
}

View file

@ -117,4 +117,15 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
repository.clearDeletedHistory()
}
fun getRecordsBetweenTwoItems(item1: Long, item2: Long) : List<HistoryItem> {
val filtered = repository.getFiltered(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
val 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 }
}
}
}

View file

@ -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<List<ResultItem>>
private val infoUtil: InfoUtil
private val notificationUtil: NotificationUtil
private val dao: ResultDao
data class ResultsUiState(
var processing: Boolean,
var errorMessage: Pair<Int, String>?,
@ -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<Application>().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<ResultItem>{
return dao.getResultsBetweenTwoItems(item1, item2)
}
}

View file

@ -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<ResultItem?>? = null
private var selectedObjects: ArrayList<ResultItem>? = null
private lateinit var selectedObjects: ArrayList<ResultItem>
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<ResultItem>()
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()
}

View file

@ -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<Pair<DownloadItem, Long?>, 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<Button>(R.id.already_exists_edit)
val deleteBtn = alreadyExistsItem.findViewById<Button>(R.id.already_exists_delete)
val card = holder.cardView
card.tag = item.id.toString()
card.popup()
val ttle = alreadyExistsItem.findViewById<Button>(R.id.already_exists_title)
ttle.text = item.first.title
val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById<ImageView>(R.id.downloads_image_view)
CoroutineScope(Dispatchers.IO).launch {
// THUMBNAIL ----------------------------------
val hideThumb = sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue")
uiHandler.post { thumbnail.loadThumbnail(hideThumb, item.thumb) }
val duration = card.findViewById<TextView>(R.id.duration)
duration.text = item.duration
// TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.title)
var title = item.title
if (title.length > 100) {
title = title.substring(0, 40) + "..."
}
itemTitle.text = title.ifEmpty { item.url }
val formatNote = card.findViewById<TextView>(R.id.format_note)
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
View.GONE
else formatNote!!.text = item.format.format_note.uppercase()
val codec = card.findViewById<TextView>(R.id.codec)
val codecText =
if (item.format.encoding != "") {
item.format.encoding.uppercase()
}else if (item.format.vcodec != "none" && item.format.vcodec != ""){
item.format.vcodec.uppercase()
} else {
item.format.acodec.uppercase()
}
if (codecText == "" || codecText == "none"){
codec.visibility = View.GONE
}else{
codec.visibility = View.VISIBLE
codec.text = codecText
}
ttle.setOnLongClickListener {
onItemClickListener.onDeleteItem(item.first, position, item.second)
val fileSize = card.findViewById<TextView>(R.id.file_size)
val fileSizeReadable = FileUtil.convertFileSize(item.format.filesize)
if (fileSizeReadable == "?") fileSize.visibility = View.GONE
else fileSize.text = fileSizeReadable
val editBtn = card.findViewById<Button>(R.id.already_exists_edit)
val deleteBtn = card.findViewById<Button>(R.id.already_exists_delete)
card.setOnLongClickListener {
onItemClickListener.onDeleteItem(item, position, historyID)
true
}
editBtn.setOnClickListener {
onItemClickListener.onEditItem(item.first, position)
onItemClickListener.onEditItem(item, position)
}
deleteBtn.setOnClickListener {
onItemClickListener.onDeleteItem(item.first, position, item.second)
onItemClickListener.onDeleteItem(item, position, historyID)
}
ttle.setOnClickListener(null)
if (item.second != null){
ttle.setOnClickListener {
onItemClickListener.onShowHistoryItem(item.second!!)
card.setOnClickListener(null)
if (historyID != null){
card.setOnClickListener {
onItemClickListener.onShowHistoryItem(historyID)
}
}

View file

@ -181,6 +181,13 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit
onItemClickListener.onCardSelect(itemID, card.isChecked)
}
@SuppressLint("NotifyDataSetChanged")
fun checkMultipleItems(list: List<Long>){
checkedItems.clear()
checkedItems.addAll(list)
notifyDataSetChanged()
}
interface OnItemClickListener {
fun onCardClick(itemID: Long, isPresent: Boolean)
fun onButtonClick(itemID: Long, isPresent: Boolean)

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.adapter
import android.annotation.SuppressLint
import android.app.Activity
import android.content.SharedPreferences
import android.os.Handler
@ -22,19 +23,18 @@ import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.squareup.picasso.Picasso
class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<ResultItem?, HomeAdapter.ViewHolder>(AsyncDifferConfig.Builder(
DIFF_CALLBACK
).build()) {
private val checkedVideos: ArrayList<String>
private val checkedItems: ArrayList<String>
private val onItemClickListener: OnItemClickListener
private val activity: Activity
private val sharedPreferences: SharedPreferences
init {
checkedVideos = ArrayList()
checkedItems = ArrayList()
this.onItemClickListener = onItemClickListener
this.activity = activity
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
@ -132,7 +132,7 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
// videoBtn.setIcon(ContextCompat.getDrawable(activity, R.drawable.ic_video));
// }
// }
if (checkedVideos.contains(videoURL)) {
if (checkedItems.contains(videoURL)) {
card.isChecked = true
card.strokeWidth = 5
} else {
@ -145,7 +145,7 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
true
}
card.setOnClickListener {
if (checkedVideos.size > 0) {
if (checkedItems.size > 0) {
checkCard(card, videoURL)
}else{
onItemClickListener.onCardDetailsClick(videoURL)
@ -156,10 +156,10 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
private fun checkCard(card: MaterialCardView, videoURL: String) {
if (card.isChecked) {
card.strokeWidth = 0
checkedVideos.remove(videoURL)
checkedItems.remove(videoURL)
} else {
card.strokeWidth = 5
checkedVideos.add(videoURL)
checkedItems.add(videoURL)
}
card.isChecked = !card.isChecked
onItemClickListener.onCardClick(videoURL, card.isChecked)
@ -173,23 +173,30 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
}
fun checkAll(items: List<ResultItem?>?){
checkedVideos.clear()
checkedVideos.addAll(items!!.map { it!!.url })
checkedItems.clear()
checkedItems.addAll(items!!.map { it!!.url })
notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged")
fun checkMultipleItems(list: List<String>){
checkedItems.clear()
checkedItems.addAll(list)
notifyDataSetChanged()
}
fun invertSelected(items: List<ResultItem?>?){
val invertedList = mutableListOf<String>()
items?.forEach {
if (!checkedVideos.contains(it!!.url)) invertedList.add(it.url)
if (!checkedItems.contains(it!!.url)) invertedList.add(it.url)
}
checkedVideos.clear()
checkedVideos.addAll(invertedList)
checkedItems.clear()
checkedItems.addAll(invertedList)
notifyDataSetChanged()
}
fun clearCheckedItems(){
checkedVideos.clear()
checkedItems.clear()
}
companion object {

View file

@ -22,6 +22,7 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.util.Extensions.loadThumbnail
import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.card.MaterialCardView
import com.squareup.picasso.Picasso
import java.util.*
@ -42,10 +43,10 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
}
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
val cardView: ConstraintLayout
val cardView: MaterialCardView
init {
cardView = itemView.findViewById(R.id.playlist_card_constraintLayout)
cardView = itemView.findViewById(R.id.playlist_card)
}
}

View file

@ -105,6 +105,14 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ
notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged")
fun checkMultipleItems(list: List<Long>){
checkedItems.clear()
checkedItems.addAll(list)
notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged")
fun invertSelected(items: List<CommandTemplate?>?){
val invertedList = mutableListOf<Long>()

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Dialog
import android.content.DialogInterface
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
@ -69,7 +70,10 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
behavior.peekHeight = displayMetrics.heightPixels - 400
if(resources.getBoolean(R.bool.isTablet) || resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.peekHeight = displayMetrics.heightPixels
}
}
tabLayout = view.findViewById(R.id.download_tablayout)

View file

@ -438,8 +438,8 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
}
//replace existing chip to enable click events
if (selectedCuts.contains(it.title)){
val idx = selectedCuts.indexOf(it.title)
val idx = selectedCuts.indexOfFirst { c -> c.contains(it.title) }
if (idx > -1){
val chipForDeletion = chipGroup.children.firstOrNull { cc -> (cc as Chip).text == it.title }
chipGroup.removeView(chipForDeletion)
createChapterChip(it, idx)
@ -456,7 +456,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
player.seekTo(0)
suggestedChips.children.apply {
this.forEach {
it.isVisible = ! selectedCuts.contains((it as Chip).text)
it.isVisible = ! selectedCuts.any { c -> c.contains((it as Chip).text) }
}
suggestedLabel.isVisible = this.any { it.isVisible }
}
@ -527,15 +527,17 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
private fun createChapterChip(chapter: ChapterItem, position: Int?) : Chip {
val chip = layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
chip.text = chapter.title
val timestamp = "${chapter.start_time.toInt().toStringDuration(Locale.US)}-${chapter.end_time.toInt().toStringDuration(Locale.US)} [${chapter.title}]"
chip.text = timestamp
chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK))
chip.isCheckedIconVisible = false
if (position != null) chipGroup.addView(chip, position)
else chipGroup.addView(chip)
if (! selectedCuts.contains(chapter.title))
selectedCuts.add(chip.text.toString())
if (! selectedCuts.contains(timestamp))
selectedCuts.add(timestamp)
listener.onChangeCut(selectedCuts)
if (chapter.start_time == 0L && chapter.end_time == 0L) {

View file

@ -305,8 +305,10 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
scheduleBtn.setOnClickListener{
UiUtil.showDatePicker(fragmentManager) {
lifecycleScope.launch {
resultViewModel.cancelUpdateItemData()
resultViewModel.cancelUpdateFormatsItemData()
withContext(Dispatchers.IO){
resultViewModel.cancelUpdateItemData()
resultViewModel.cancelUpdateFormatsItemData()
}
}
scheduleBtn.isEnabled = false
@ -704,8 +706,8 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
lifecycleScope.launch {
resultViewModel.cancelUpdateItemData()
resultViewModel.cancelUpdateFormatsItemData()
super.onDismiss(dialog)
}
super.onDismiss(dialog)
}
}

View file

@ -222,8 +222,8 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
val listener = object : OnFormatClickListener {
override fun onFormatClick(item: List<FormatTuple>) {
downloadItem.format = item.first().format
downloadItem.videoPreferences.audioFormatIDs.clear()
item.first().audioFormats?.map { it.format_id }?.let {
downloadItem.videoPreferences.audioFormatIDs.clear()
downloadItem.videoPreferences.audioFormatIDs.addAll(it)
}
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format,

View file

@ -15,6 +15,7 @@ import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
@ -499,7 +500,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
}else{
val selectedFormats = mutableListOf<Format>()
formatCollection.forEach {
selectedFormats.add(it.first{ f -> f.format_id == format.format_id})
selectedFormats.add(it.firstOrNull{ f -> f.format_id == format.format_id} ?: format)
}
if (selectedFormats.isEmpty()) {
items.forEach { _ ->

View file

@ -1,15 +1,20 @@
package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint
import android.app.ActionBar.LayoutParams
import android.app.Dialog
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
@ -27,50 +32,38 @@ import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.setTextAndRecalculateWidth
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.absoluteValue
class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickListener {
class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.OnItemClickListener {
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var resultViewModel: ResultViewModel
private lateinit var listAdapter : PlaylistAdapter
private lateinit var recyclerView: RecyclerView
private lateinit var ok: MenuItem
private lateinit var toolbar: MaterialToolbar
private lateinit var ok: MaterialButton
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var fromTextInput: TextInputLayout
private lateinit var toTextInput: TextInputLayout
private lateinit var count: TextView
private lateinit var selectBetween: MenuItem
private lateinit var items: List<ResultItem?>
private lateinit var itemURLs: List<String>
private lateinit var type: DownloadViewModel.Type
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.FullScreenDialogTheme)
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.select_playlist_items, container, false)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
return dialog
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= 33){
arguments?.getParcelableArrayList("results", ResultItem::class.java)
@ -85,11 +78,24 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
}
}
type = arguments?.getSerializable("type") as DownloadViewModel.Type
}
@SuppressLint("RestrictedApi", "NotifyDataSetChanged")
override fun setupDialog(dialog: Dialog, style: Int) {
super.setupDialog(dialog, style)
val view = LayoutInflater.from(context).inflate(R.layout.select_playlist_items, null)
dialog.setContentView(view)
dialog?.window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
if(resources.getBoolean(R.bool.isTablet) || resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.peekHeight = displayMetrics.heightPixels
}
}
toolbar = view.findViewById<MaterialToolbar>(R.id.toolbar)
listAdapter =
PlaylistAdapter(
@ -103,7 +109,8 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
recyclerView.enableFastScroll()
listAdapter.submitList(items)
toolbar.title = "0 ${resources.getString(R.string.selected)}"
count = view.findViewById(R.id.count)
count.text = "0 ${resources.getString(R.string.selected)}"
fromTextInput = view.findViewById(R.id.from_textinput)
toTextInput = view.findViewById(R.id.to_textinput)
@ -124,7 +131,7 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
if (endNr > items.size) endNr = items.size - 1
listAdapter.checkRange(startNr, endNr)
ok.isEnabled = true
toolbar.title = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}"
count.text = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}"
}
}
}
@ -144,12 +151,12 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
if (endNr > items.size) endNr = items.size -1
listAdapter.checkRange(startNr, endNr)
ok.isEnabled = true
toolbar.title = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}"
count.text = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}"
}
}
}
val checkAll = view.findViewById<ExtendedFloatingActionButton>(R.id.check_all)
val checkAll = view.findViewById<FloatingActionButton>(R.id.check_all)
checkAll!!.setOnClickListener {
if (listAdapter.getCheckedItems().size != items.size){
fromTextInput.editText!!.setTextAndRecalculateWidth("1")
@ -158,7 +165,7 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
fromTextInput.isEnabled = true
toTextInput.isEnabled = true
ok.isEnabled = true
toolbar.title = resources.getString(R.string.all_items_selected)
count.text = resources.getString(R.string.all_items_selected)
}else{
reset()
fromTextInput.isEnabled = true
@ -170,9 +177,9 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
}
ok = toolbar.menu.getItem(0)
ok = view.findViewById(R.id.bottomsheet_ok_button)
ok.isEnabled = false
ok.setOnMenuItemClickListener {
ok.setOnClickListener {
ok.isEnabled = false
lifecycleScope.launch(Dispatchers.IO) {
val checkedItems = listAdapter.getCheckedItems()
@ -200,7 +207,7 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
downloadViewModel.insertToProcessing(downloadItems)
withContext(Dispatchers.Main){
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog)
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadMultipleBottomSheetDialog)
}
}
@ -210,30 +217,45 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
true
}
view.findViewById<BottomAppBar>(R.id.bottomAppBar).setOnMenuItemClickListener { m: MenuItem ->
val itemId = m.itemId
if (itemId == R.id.invert_selected) {
listAdapter.invertSelected(items)
val checkedItems = listAdapter.getCheckedItems()
if (checkedItems.size == items.size){
toolbar.title= resources.getString(R.string.all_items_selected)
}else{
toolbar.title = "${checkedItems.size} ${resources.getString(R.string.selected)}"
val bottomAppBar = view.findViewById<BottomAppBar>(R.id.bottomAppBar)
bottomAppBar.setOnMenuItemClickListener { m: MenuItem ->
when(m.itemId) {
R.id.invert_selected -> {
listAdapter.invertSelected(items)
val checkedItems = listAdapter.getCheckedItems()
if (checkedItems.size == items.size){
count.text = resources.getString(R.string.all_items_selected)
}else{
count.text = "${checkedItems.size} ${resources.getString(R.string.selected)}"
}
if(checkedItems.isNotEmpty() && checkedItems.size < items.size){
fromTextInput.isEnabled = false
toTextInput.isEnabled = false
}
ok.isEnabled = checkedItems.isNotEmpty()
}
if(checkedItems.isNotEmpty() && checkedItems.size < items.size){
fromTextInput.isEnabled = false
toTextInput.isEnabled = false
R.id.select_between -> {
val selectedItems = listAdapter.getCheckedItems()
if(selectedItems.size != 2){
m.isVisible = false
}else{
val item2 = itemURLs.indexOf(selectedItems.last())
val item1 = itemURLs.indexOf(selectedItems.first())
if(item1 > item2) listAdapter.checkRange(item2, item1)
else listAdapter.checkRange(item1, item2)
count.text = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}"
}
}
ok.isEnabled = checkedItems.isNotEmpty()
}
true
}
toolbar.setNavigationOnClickListener {
dismiss()
}
selectBetween = bottomAppBar.menu.findItem(R.id.select_between)
itemURLs = items.map { it!!.url }
}
private fun checkRanges(start: String, end: String) : Boolean {
return start.isNotBlank() && end.isNotBlank()
}
@ -241,15 +263,15 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
private fun reset(){
ok.isEnabled = false
listAdapter.clearCheckeditems()
toolbar.title = "0 ${resources.getString(R.string.selected)}"
count.text = "0 ${resources.getString(R.string.selected)}"
}
override fun onCardSelect(itemURL: String, isChecked: Boolean, checkedItems: List<String>) {
if (checkedItems.size == items.size){
toolbar.title = resources.getString(R.string.all_items_selected)
count.text = resources.getString(R.string.all_items_selected)
}else{
toolbar.title = "${checkedItems.size} ${resources.getString(R.string.selected)}"
count.text = "${checkedItems.size} ${resources.getString(R.string.selected)}"
}
if (checkedItems.isEmpty()){
ok.isEnabled = false
@ -259,6 +281,17 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
ok.isEnabled = true
fromTextInput.isEnabled = false
toTextInput.isEnabled = false
val canSelectBetween = run {
val size = checkedItems.size
if(size != 2) false
else {
val item1 = itemURLs.indexOf(checkedItems.first())
val item2 = itemURLs.indexOf(checkedItems.last())
(item1-item2).absoluteValue > 1
}
}
selectBetween.isVisible = canSelectBetween
}
}

View file

@ -160,6 +160,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
this.finish()
}else{
this.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.select_between).isVisible = false
if (selectedObjects == 2){
val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){

View file

@ -161,6 +161,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
this.finish()
}else{
actionMode?.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.select_between).isVisible = false
if(selectedObjects == 2){
val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
@ -213,7 +214,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
lifecycleScope.launch {
val selectedIDs = getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Cancelled).toListString())
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Error).toListString())
}.toMutableList()
idsInMiddle.addAll(selectedIDs)
if (idsInMiddle.isNotEmpty()){

View file

@ -86,7 +86,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
private var websiteGroup: ChipGroup? = null
private var historyList: List<HistoryItem?>? = null
private var allhistoryList: List<HistoryItem?>? = null
private var selectedObjects: ArrayList<HistoryItem>? = null
private lateinit var selectedObjects: ArrayList<HistoryItem>
private var actionMode : ActionMode? = null
private lateinit var sortChip: Chip
@ -113,7 +113,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
websiteGroup = view.findViewById(R.id.website_chip_group)
uiHandler = Handler(Looper.getMainLooper())
sortChip = view.findViewById(R.id.sortChip)
selectedObjects = ArrayList()
selectedObjects = arrayListOf()
historyList = mutableListOf()
@ -465,21 +465,26 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
}
override fun onCardSelect(itemID: Long, isChecked: Boolean) {
val item = findItem(itemID)
if (isChecked) {
selectedObjects!!.add(item!!)
if (actionMode == null){
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
lifecycleScope.launch {
val item = findItem(itemID)
if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
actionMode?.apply {
if (isChecked) selectedObjects.add(item!!)
else selectedObjects.remove(item!!)
}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()
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){
historyViewModel.getRecordsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id)
}.toMutableList()
this.menu.findItem(R.id.select_between).isVisible = resultsInMiddle.isNotEmpty()
}
}
}
}
}
@ -491,7 +496,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.history_menu_context, menu)
mode.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
mode.title = "${selectedObjects.size} ${getString(R.string.selected)}"
(activity as MainActivity).disableBottomNavigation()
topAppBar!!.menu.forEach { it.isEnabled = false }
return true
@ -509,6 +514,21 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
item: MenuItem?
): Boolean {
return when (item!!.itemId) {
R.id.select_between -> {
lifecycleScope.launch {
val selectedIDs = selectedObjects.sortedBy { it.id }
val resultsInMiddle = withContext(Dispatchers.IO){
historyViewModel.getRecordsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id)
}.toMutableList()
if (resultsInMiddle.isNotEmpty()){
selectedObjects.addAll(resultsInMiddle)
historyAdapter?.checkMultipleItems(selectedObjects.map { it.id })
actionMode?.title = "${selectedObjects.count()} ${getString(R.string.selected)}"
}
mode?.menu?.findItem(R.id.select_between)?.isVisible = false
}
true
}
R.id.delete_results -> {
val deleteFile = booleanArrayOf(false)
val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!)
@ -519,7 +539,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b }
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){
for (obj in selectedObjects){
historyViewModel.delete(obj, deleteFile[0])
}
clearCheckedItems()
@ -529,15 +549,15 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
true
}
R.id.share -> {
UiUtil.shareFileIntent(requireContext(), selectedObjects!!.map { it.downloadPath }.flatten())
UiUtil.shareFileIntent(requireContext(), selectedObjects.map { it.downloadPath }.flatten())
clearCheckedItems()
actionMode?.finish()
true
}
R.id.select_all -> {
historyAdapter?.checkAll(historyList)
selectedObjects?.clear()
historyList?.forEach { selectedObjects?.add(it!!) }
selectedObjects.clear()
historyList?.forEach { selectedObjects.add(it!!) }
mode?.title = getString(R.string.all_items_selected)
true
}
@ -545,11 +565,11 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyAdapter?.invertSelected(historyList)
val invertedList = arrayListOf<HistoryItem>()
historyList?.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
}
@ -567,7 +587,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
private fun clearCheckedItems(){
historyAdapter?.clearCheckeditems()
selectedObjects?.clear()
selectedObjects.clear()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback =

View file

@ -193,6 +193,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
}
this.menu.findItem(R.id.up).isVisible = position > 0
this.menu.findItem(R.id.select_between).isVisible = false
if(selectedObjects == 2){
val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
@ -254,7 +255,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
lifecycleScope.launch {
val selectedIDs = getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Cancelled).toListString())
downloadViewModel.getQueuedIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last())
}.toMutableList()
idsInMiddle.addAll(selectedIDs)
if (idsInMiddle.isNotEmpty()){

View file

@ -159,6 +159,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
this.finish()
}else{
actionMode?.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.select_between).isVisible = false
if(selectedObjects == 2){
val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
@ -207,7 +208,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
lifecycleScope.launch {
val selectedIDs = getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Cancelled).toListString())
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Saved).toListString())
}.toMutableList()
idsInMiddle.addAll(selectedIDs)
if (idsInMiddle.isNotEmpty()){

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.more
import android.annotation.SuppressLint
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
@ -62,7 +63,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
private lateinit var noResults: RelativeLayout
private lateinit var mainActivity: MainActivity
private lateinit var sortChip: Chip
private var selectedObjects: ArrayList<CommandTemplate>? = null
private lateinit var selectedObjects: ArrayList<CommandTemplate>
private var actionMode : ActionMode? = null
private val jsonFormat = Json { prettyPrint = true }
@ -76,6 +77,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
return inflater.inflate(R.layout.fragment_command_templates, container, false)
}
@SuppressLint("RestrictedApi")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
templatesList = listOf()
@ -273,21 +275,26 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
}
override fun onCardSelect(itemID: Long, isChecked: Boolean) {
val item = templatesList.find { it.id == itemID }
if (isChecked) {
selectedObjects!!.add(item!!)
if (actionMode == null){
actionMode = (activity as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
lifecycleScope.launch {
val item = templatesList.find { it.id == itemID }
if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
actionMode?.apply {
if (isChecked) selectedObjects.add(item!!)
else selectedObjects.remove(item!!)
}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()
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){
commandTemplateViewModel.getRecordsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id)
}.toMutableList()
this.menu.findItem(R.id.select_between).isVisible = resultsInMiddle.isNotEmpty()
}
}
}
}
}
@ -295,7 +302,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.templates_menu_context, menu)
mode.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
mode.title = "${selectedObjects.size} ${getString(R.string.selected)}"
return true
}
@ -311,12 +318,27 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
item: MenuItem?
): Boolean {
return when (item!!.itemId) {
R.id.select_between -> {
lifecycleScope.launch {
val selectedIDs = selectedObjects.sortedBy { it.id }
val resultsInMiddle = withContext(Dispatchers.IO){
commandTemplateViewModel.getRecordsBetweenTwoItems(selectedIDs.first().id, selectedIDs.last().id)
}.toMutableList()
if (resultsInMiddle.isNotEmpty()){
selectedObjects.addAll(resultsInMiddle)
templatesAdapter.checkMultipleItems(selectedObjects.map { it.id })
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(requireContext())
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 ->
for (obj in selectedObjects!!){
for (obj in selectedObjects){
commandTemplateViewModel.delete(obj)
}
clearCheckedItems()
@ -327,8 +349,8 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
}
R.id.select_all -> {
templatesAdapter.checkAll(templatesList)
selectedObjects?.clear()
templatesList.forEach { selectedObjects?.add(it) }
selectedObjects.clear()
templatesList.forEach { selectedObjects.add(it) }
mode?.title = getString(R.string.all_items_selected)
true
}
@ -336,11 +358,11 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
templatesAdapter.invertSelected(templatesList)
val invertedList = arrayListOf<CommandTemplate>()
templatesList.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
}
@ -348,7 +370,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
lifecycleScope.launch{
val output = jsonFormat.encodeToString(
CommandTemplateExport(
templates = selectedObjects!!.toList(),
templates = selectedObjects.toList(),
shortcuts = listOf()
)
)
@ -374,7 +396,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
private fun clearCheckedItems(){
templatesAdapter.clearCheckeditems()
selectedObjects?.clear()
selectedObjects.clear()
}
private var simpleCallback: ItemTouchHelper.SimpleCallback =

View file

@ -30,6 +30,7 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.ui.adapter.CookieAdapter
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.chip.Chip
@ -128,7 +129,9 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
cookiesViewModel.deleteAll()
kotlin.runCatching {
File(context?.cacheDir, "cookies.txt").apply { writeText("") }
FileUtil.getCookieFile(requireContext(), true){
File(it).apply { writeText("") }
}
}
}
deleteDialog.show()

View file

@ -20,11 +20,11 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.ui.BaseActivity
import com.deniscerri.ytdlnis.util.Extensions.convertNetscapeToSetCookie
import com.google.accompanist.web.AccompanistWebChromeClient
import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.google.android.exoplayer2.util.Log
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton
@ -49,16 +49,15 @@ class WebViewActivity : BaseActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.webview_activity)
url = intent.extras!!.getString("url")!!
cookies = ""
cookiesViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
val appbar = findViewById<AppBarLayout>(R.id.webview_appbarlayout)
toolbar = appbar.findViewById(R.id.webviewToolbar)
generateBtn = toolbar.findViewById(R.id.generate)
webViewCompose = findViewById(R.id.webview_compose)
cookieManager = CookieManager.getInstance()
preferences = PreferenceManager.getDefaultSharedPreferences(this)
lifecycleScope.launch {
val appbar = findViewById<AppBarLayout>(R.id.webview_appbarlayout)
toolbar = appbar.findViewById(R.id.webviewToolbar)
generateBtn = toolbar.findViewById(R.id.generate)
webViewCompose = findViewById(R.id.webview_compose)
cookieManager = CookieManager.getInstance()
preferences = PreferenceManager.getDefaultSharedPreferences(this@WebViewActivity)
lifecycleScope.launch{
webViewClient = object : AccompanistWebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
@ -78,9 +77,7 @@ class WebViewActivity : BaseActivity() {
generateBtn.setOnClickListener {
lifecycleScope.launch {
withContext(Dispatchers.IO){
val a = cookieManager.getCookie(url).replace(";", "\n")
Log.e("TESTING", a)
withContext(Dispatchers.IO) {
cookiesViewModel.getCookiesFromDB(url).getOrNull()?.let {
kotlin.runCatching {
cookiesViewModel.insert(
@ -92,12 +89,17 @@ class WebViewActivity : BaseActivity() {
)
cookiesViewModel.updateCookiesFile()
}.onFailure {
withContext(Dispatchers.Main){
Toast.makeText(this@WebViewActivity, "Something went wrong", Toast.LENGTH_SHORT).show()
withContext(Dispatchers.Main) {
Toast.makeText(
this@WebViewActivity,
"Something went wrong",
Toast.LENGTH_SHORT
).show()
}
}
}
withContext(Dispatchers.Main){
cookieManager.removeAllCookies(null)
withContext(Dispatchers.Main) {
onBackPressedDispatcher.onBackPressed()
}
}
@ -109,7 +111,6 @@ class WebViewActivity : BaseActivity() {
}
}
}
@SuppressLint("SetJavaScriptEnabled")

View file

@ -45,7 +45,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
private lateinit var topAppBar: MaterialToolbar
private lateinit var mainActivity: MainActivity
private lateinit var logViewModel: LogViewModel
private var selectedObjects: ArrayList<LogItem>? = null
private lateinit var selectedObjects: ArrayList<LogItem>
private var actionMode : ActionMode? = null
override fun onCreateView(
@ -139,18 +139,18 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
override fun onCardSelect(itemID: Long, isChecked: Boolean) {
val item = items.find { it.id == itemID }
if (isChecked) {
selectedObjects!!.add(item!!)
selectedObjects.add(item!!)
if (actionMode == null){
actionMode = (activity as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
actionMode!!.title = "${selectedObjects.size} ${getString(R.string.selected)}"
}
}
else {
selectedObjects!!.remove(item)
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
if (selectedObjects!!.isEmpty()){
selectedObjects.remove(item)
actionMode?.title = "${selectedObjects.size} ${getString(R.string.selected)}"
if (selectedObjects.isEmpty()){
actionMode?.finish()
}
}
@ -159,7 +159,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.logs_menu_context, menu)
mode.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
mode.title = "${selectedObjects.size} ${getString(R.string.selected)}"
return true
}
@ -180,7 +180,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
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 ->
for (obj in selectedObjects!!){
for (obj in selectedObjects){
logViewModel.delete(obj)
}
clearCheckedItems()
@ -191,8 +191,8 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
}
R.id.select_all -> {
downloadLogAdapter.checkAll(items)
selectedObjects?.clear()
items.forEach { selectedObjects?.add(it) }
selectedObjects.clear()
items.forEach { selectedObjects.add(it) }
mode?.title = getString(R.string.all_items_selected)
true
}
@ -200,11 +200,11 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
downloadLogAdapter.invertSelected(items)
val invertedList = arrayListOf<LogItem>()
items.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
}
@ -220,7 +220,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
private fun clearCheckedItems(){
downloadLogAdapter.clearCheckeditems()
selectedObjects?.clear()
selectedObjects.clear()
}

View file

@ -11,6 +11,7 @@ import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
@ -30,6 +31,8 @@ class FolderSettingsFragment : BaseSettingsFragment() {
private var videoPath: Preference? = null
private var commandPath: Preference? = null
private var accessAllFiles : Preference? = null
private var noFragments: SwitchPreferenceCompat? = null
private var keepFragments: SwitchPreferenceCompat? = null
private var cacheDownloads : Preference? = null
private var audioFilenameTemplate : Preference? = null
private var videoFilenameTemplate : Preference? = null
@ -50,6 +53,8 @@ class FolderSettingsFragment : BaseSettingsFragment() {
videoPath = findPreference("video_path")
commandPath = findPreference("command_path")
accessAllFiles = findPreference("access_all_files")
noFragments = findPreference("no_part")
keepFragments = findPreference("keep_cache")
cacheDownloads = findPreference("cache_downloads")
videoFilenameTemplate = findPreference("file_name_template")
audioFilenameTemplate = findPreference("file_name_template_audio")
@ -116,6 +121,17 @@ class FolderSettingsFragment : BaseSettingsFragment() {
true
}
noFragments!!.setOnPreferenceChangeListener { _, newValue ->
if(newValue as Boolean){
editor.putBoolean("keep_cache", false).apply()
keepFragments!!.isChecked = false
keepFragments!!.isEnabled = false
}else{
keepFragments!!.isEnabled = true
}
true
}
videoFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
videoFilenameTemplate?.summary = preferences.getString("file_name_template", "%(uploader)s - %(title)s")
audioFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]"
@ -171,7 +187,7 @@ class FolderSettingsFragment : BaseSettingsFragment() {
if (list.first() == null) return@observe
if (list.first().state == WorkInfo.State.SUCCEEDED){
cacheSize = File(requireContext().cacheDir.absolutePath + "/downloads").walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
cacheSize = File(FileUtil.getCachePath(requireContext())).walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}"
}
}

View file

@ -0,0 +1,46 @@
package com.deniscerri.ytdlnis.util
import android.content.Context
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.LogDao
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlin.system.exitProcess
class CrashListener(private val context: Context) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(p0: Thread, p1: Throwable) {
p1.message?.apply {
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
createLog(this@apply)
}
}
}
private suspend fun createLog(message: String){
kotlin.runCatching {
val db = DBManager.getInstance(context)
val dao = db.logDao
dao.insert(LogItem(
id = 0L,
title = "APP CRASH",
content = message,
format = Format("", "", "", "", "", 0, "", "", "", "", "", ""),
downloadType = DownloadViewModel.Type.command,
downloadTime = System.currentTimeMillis()
))
}
exitProcess(0)
}
fun registerExceptionHandler(){
Thread.setDefaultUncaughtExceptionHandler(this)
}
}

View file

@ -27,6 +27,7 @@ import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -40,6 +41,7 @@ import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import org.json.JSONObject
import java.io.File
import java.net.HttpCookie
import java.util.Locale
import java.util.regex.Pattern
import kotlin.math.abs
@ -225,22 +227,26 @@ object Extensions {
number = nr
verticalOffset = 3
horizontalOffset =
if (nr < 10) 7
else if (nr < 100) 10
else 20
if (nr < 10) dp(App.instance.resources, 7f)
else if (nr < 100) dp(App.instance.resources, 10f)
else dp(App.instance.resources, 20f)
}
}
}
fun String.appendLineToLog(line: String): String {
val lines = this.split("\n")
//clean dublicate progress + add newline
var newLine = line
if (newLine.contains("[download")) {
newLine = "[download]" + line.split("[download]").last()
if (!lines.takeLast(3).contains(line)){
//clean dublicate progress + add newline
var newLine = line
if (newLine.contains("[download")) {
newLine = "[download]" + line.split("[download]").last()
}
return lines.dropLastWhile { it.contains("[download") }.joinToString("\n") + "\n${newLine}"
}
return lines.dropLastWhile { it.contains("[download") }.joinToString("\n") + "\n${newLine}"
return this
}
fun ImageView.loadThumbnail(hideThumb: Boolean, imageURL: String){
@ -272,5 +278,55 @@ object Extensions {
}
}
fun String.convertToTimestamp() : Int {
return try {
val timeArray = this.split(":")
var timeSeconds = timeArray[timeArray.lastIndex].toInt()
var times = 60
for (i in timeArray.lastIndex - 1 downTo 0) {
timeSeconds += timeArray[i].toInt() * times
times *= 60
}
timeSeconds
}catch (e: Exception){
e.printStackTrace()
0
}
}
fun String.convertNetscapeToSetCookie(): String {
// Split the Netscape cookie string
val parts =
this.split("\t").dropLastWhile { it.isEmpty() }
.toTypedArray()
if (parts.isEmpty()) return ""
// Extract the individual components
val domain = parts[0].trim { it <= ' ' }
val isSecure =
java.lang.Boolean.parseBoolean(parts[3].trim { it <= ' ' })
val expiry = parts[4].trim { it <= ' ' }.toLong()
val name = parts[5].trim { it <= ' ' }
val value = parts[6].trim { it <= ' ' }
// Create the BasicClientCookie
val cookie = HttpCookie(name, value)
cookie.domain = domain
cookie.path = "/"
cookie.secure = isSecure
// Set expiry
if (expiry != 0L) {
cookie.maxAge = expiry
} else {
// For session cookies, set to null
cookie.maxAge = -1L
}
// Get the Set-Cookie header format
return cookie.toString()
}
}

View file

@ -262,15 +262,22 @@ object FileUtil {
}
fun getCachePath(context: Context) : String {
return context.cacheDir.absolutePath + "/downloads/"
val externalPath = context.getExternalFilesDir(null)
return if (externalPath == null){
context.cacheDir.absolutePath + "/downloads/"
}else{
externalPath.absolutePath + "/downloads/"
}
}
fun deleteConfigFiles(request: YoutubeDLRequest) {
request.getArguments("--config")?.forEach {
if (it != null) File(it).delete()
}
request.getArguments("--config-locations")?.forEach {
if (it != null) File(it).delete()
runCatching {
request.getArguments("--config")?.forEach {
if (it != null) File(it).delete()
}
request.getArguments("--config-locations")?.forEach {
if (it != null) File(it).delete()
}
}
}
@ -287,16 +294,18 @@ object FileUtil {
}
fun getDownloadArchivePath(context: Context) : String {
return context.cacheDir.absolutePath + "/download_archive.txt"
return context.filesDir.absolutePath + "/download_archive.txt"
}
fun getDefaultTerminalPath() : String {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/TERMINAL_CACHE"
}
fun getCookieFile(context : Context, path: (path: String) -> Unit){
fun getCookieFile(context : Context, ignoreIfExists: Boolean = false, path: (path: String) -> Unit){
val cookiesFile = File(context.cacheDir, "cookies.txt")
if (cookiesFile.exists()) path(cookiesFile.absolutePath)
if (ignoreIfExists || cookiesFile.exists()){
path(cookiesFile.absolutePath)
}
}
fun convertFileSize(s: Long): String{

View file

@ -249,8 +249,8 @@ class InfoUtil(private val context: Context) {
formats.groupBy { it.format_id }.forEach {
if (it.value.count() > 1) {
it.value.filter { f-> !f.format_note.contains("original", true) }.forEachIndexed { index, format -> format.format_id = format.format_id.split("-")[0] + "-${index}" }
val engDefault = it.value.find { f -> f.format_note.contains("original", true) }
engDefault?.format_id = (engDefault?.format_id?.split("-")?.get(0) ?: "") + "-${it.value.size-1}"
val defaultLang = it.value.find { f -> f.format_note.contains("original", true) }
defaultLang?.format_id = (defaultLang?.format_id?.split("-")?.get(0) ?: "") + "-${it.value.size-1}"
}
}
formats.sortByDescending { it.filesize }
@ -648,9 +648,9 @@ class InfoUtil(private val context: Context) {
if (result.isNullOrBlank()) continue
val jsonObject = JSONObject(result)
val title = jsonObject.getStringByAny("track", "alt_title", "title", "webpage_url_basename")
if (title == "[Private video]") continue
if (title == "[Private video]" || title == "[Deleted video]") continue
val author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id")
var author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id")
var duration = jsonObject.getIntByAny("duration").toString()
if (duration != "-1"){
duration = jsonObject.getInt("duration").toStringDuration(Locale.US)
@ -666,6 +666,17 @@ class InfoUtil(private val context: Context) {
}
}
if(author.isEmpty()){
runCatching {
val firstEntry = jsonObject.getJSONArray("entries").getJSONObject(0)
author = firstEntry.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id")
val thumbs = firstEntry.getJSONArray("thumbnails")
if (thumbs.length() > 0){
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
}
}
}
val website = jsonObject.getStringByAny("ie_key", "extractor_key", "extractor")
var playlistTitle = jsonObject.getStringByAny("playlist_title")
var playlistURL: String? = ""
@ -991,11 +1002,15 @@ class InfoUtil(private val context: Context) {
val keepCache = sharedPreferences.getBoolean("keep_cache", false)
if(keepCache){
request.addOption("--part")
request.addOption("--keep-fragments")
}
if (sharedPreferences.getBoolean("no_part", false)){
request.addOption("--no-part")
}
val embedMetadata = sharedPreferences.getBoolean("embed_metadata", true)
val thumbnailFormat = sharedPreferences.getString("thumbnail_format", "jpg")
var filenameTemplate = downloadItem.customFileNameTemplate
if(downloadItem.type != DownloadViewModel.Type.command){
@ -1003,7 +1018,7 @@ class InfoUtil(private val context: Context) {
if (downloadItem.SaveThumb) {
request.addOption("--write-thumbnail")
request.addOption("--convert-thumbnails", sharedPreferences.getString("thumbnail_format", "jpg")!!)
request.addOption("--convert-thumbnails", thumbnailFormat!!)
}
if (!sharedPreferences.getBoolean("mtime", false)){
request.addOption("--no-mtime")
@ -1032,22 +1047,13 @@ class InfoUtil(private val context: Context) {
}
}
if (downloadItem.playlistTitle.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "video:playlist", ".+", downloadItem.playlistTitle))
runCatching {
if (downloadItem.playlistIndex != null){
request.addOption("--parse-metadata", downloadItem.playlistIndex.toString() + ":%(playlist_index)s")
}
}
}
if(downloadItem.title.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.take(150)))
request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.replace("\"", "\\\"").take(150)))
}
if (downloadItem.author.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "video:uploader", ".+", downloadItem.author.take(30)))
request.addCommands(listOf("--replace-in-metadata", "video:uploader", ".+", downloadItem.author.replace("\"", "\\\"").take(30)))
}
request.addOption("--parse-metadata", "uploader:(?P<uploader>.+)(?: - Topic)$")
@ -1059,10 +1065,7 @@ class InfoUtil(private val context: Context) {
if (downloadItem.downloadSections.isNotBlank()){
downloadItem.downloadSections.split(";").forEach {
if (it.isBlank()) return@forEach
if (it.contains(":"))
request.addOption("--download-sections", "*$it")
else
request.addOption("--download-sections", it)
request.addOption("--download-sections", "*${it.split(" ")[0]}")
if (sharedPreferences.getBoolean("force_keyframes", false) && !request.hasOption("--force-keyframes-at-cuts")){
request.addOption("--force-keyframes-at-cuts")
@ -1121,6 +1124,13 @@ class InfoUtil(private val context: Context) {
val ext = downloadItem.container
if (audioQualityId.isNotBlank()) {
if(audioQualityId.contains("-")){
audioQualityId = if(!downloadItem.format.lang.isNullOrBlank() && downloadItem.format.lang != "None"){
"ba[format_id~='^(${audioQualityId.split("-")[0]})'][language^=${downloadItem.format.lang}]/ba/b"
}else{
"$audioQualityId/${audioQualityId.split("-")[0]}"
}
}
if (audioQualityId.contains("-") && !downloadItem.format.lang.isNullOrBlank() && downloadItem.format.lang != "None"){
audioQualityId = "ba[format_id~='^(${audioQualityId.split("-")[0]})'][language^=${downloadItem.format.lang}]/ba/b"
}
@ -1163,26 +1173,26 @@ class InfoUtil(private val context: Context) {
if (downloadItem.playlistTitle.isNotEmpty()) {
request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s")
request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(meta_track)s")
request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(meta_track_number)s")
} else {
request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
}
}
val cropThumb = downloadItem.audioPreferences.cropThumb ?: sharedPreferences.getBoolean("crop_thumbnail", true)
if (thumbnailFormat == "jpg"){
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
}
if (downloadItem.audioPreferences.embedThumb) {
if (downloadItem.audioPreferences.embedThumb){
request.addOption("--embed-thumbnail")
if (! request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", "jpg")
val cropThumb = downloadItem.audioPreferences.cropThumb ?: sharedPreferences.getBoolean("crop_thumbnail", true)
if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!)
if (cropThumb){
try {
runCatching {
val config = File(context.cacheDir.absolutePath + "/config" + downloadItem.id + "##ffmpegCrop.txt")
val configData = "--ppa \"ffmpeg:-c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
val configData = "--ppa \"ThumbnailsConvertor:-vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
config.writeText(configData)
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
request.addOption("--config", config.absolutePath)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@ -1440,7 +1450,7 @@ class InfoUtil(private val context: Context) {
val acodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac")
audioFormats.forEachIndexed { idx, it -> formats.add(Format(audioFormatsValues[idx], containerPreference!!,"",acodecPreference!!, "",0, it)) }
audioFormats.reverse()
audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",acodecPreference!!, "",1, it)) }
audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",resources.getString(R.string.preferred_format_id), "",1, it)) }
return formats
}
@ -1449,10 +1459,24 @@ class InfoUtil(private val context: Context) {
val videoFormats = resources.getStringArray(R.array.video_formats_values)
val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("video_format", "")
val acodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac")
val vcodecPreference = sharedPreferences.getString("video_codec", "avc|h264")!!.ifEmpty { resources.getString(R.string.defaultValue) }
videoFormats.reversed().forEach { formats.add(Format(it, containerPreference!!,vcodecPreference,acodecPreference!!, "",0, it.split("_")[0])) }
formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,vcodecPreference,acodecPreference!!, "",1, it)) }
val audioCodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac")!!.run {
if (this.isNotEmpty()){
val audioCodecs = resources.getStringArray(R.array.audio_codec)
val audioCodecsValues = resources.getStringArray(R.array.audio_codec_values)
audioCodecs[audioCodecsValues.indexOf(this)]
}else this
}
val videoCodecPreference = sharedPreferences.getString("video_codec", "avc|h264")!!.run {
if (this.isEmpty()){
resources.getString(R.string.defaultValue)
}else{
val videoCodecs = resources.getStringArray(R.array.video_codec)
val videoCodecsValues = resources.getStringArray(R.array.video_codec_values)
videoCodecs[videoCodecsValues.indexOf(this)]
}
}
videoFormats.reversed().forEach { formats.add(Format(it, containerPreference!!,videoCodecPreference,audioCodecPreference, "",0, it.split("_")[0])) }
formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.preferred_format_id),"", "",1, it)) }
return formats
}

View file

@ -17,6 +17,8 @@ import androidx.core.content.FileProvider
import androidx.documentfile.provider.DocumentFile
import androidx.navigation.NavDeepLinkBuilder
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.receiver.CancelDownloadNotificationReceiver
import com.deniscerri.ytdlnis.receiver.CancelWorkReceiver
import com.deniscerri.ytdlnis.receiver.PauseDownloadNotificationReceiver
@ -221,13 +223,14 @@ class NotificationUtil(var context: Context) {
fun createDownloadFinished(
id: Long,
title: String?,
downloadType: DownloadViewModel.Type,
filepath: List<String>?,
res: Resources
) {
val notificationBuilder = getBuilder(DOWNLOAD_FINISHED_CHANNEL_ID)
notificationBuilder
.setContentTitle("${res.getString(R.string.downloaded)} $title")
.setContentTitle("${res.getString(R.string.downloaded)} (${downloadType}): $title")
.setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon(
BitmapFactory.decodeResource(
@ -237,7 +240,7 @@ class NotificationUtil(var context: Context) {
)
.setGroup(DOWNLOAD_FINISHED_NOTIFICATION_ID.toString())
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.setContentText("")
.setContentText(title)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions()
@ -387,7 +390,7 @@ class NotificationUtil(var context: Context) {
)
try {
notificationBuilder.setProgress(100, progress, false)
notificationBuilder.setProgress(100, progress, progress == 0)
.setContentTitle(title)
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
.clearActions()
@ -420,7 +423,7 @@ class NotificationUtil(var context: Context) {
)
try {
notificationBuilder.setProgress(100, progress, false)
notificationBuilder.setProgress(100, progress, progress == 0)
.setContentTitle(title)
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
.clearActions()
@ -462,7 +465,7 @@ class NotificationUtil(var context: Context) {
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
val contentText = "${progress}/${totalFiles}"
try {
notificationBuilder.setProgress(100, progress, false)
notificationBuilder.setProgress(100, progress, progress == 0)
.setContentTitle(resources.getString(R.string.move_temporary_files))
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
notificationManager.notify(id, notificationBuilder.build())
@ -534,7 +537,7 @@ class NotificationUtil(var context: Context) {
try {
notificationBuilder.setProgress(queue, progress, false)
notificationBuilder.setProgress(queue, progress, progress == 0)
.setContentTitle(resources.getString(R.string.update_formats_background))
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
.clearActions()

View file

@ -111,6 +111,9 @@ object UiUtil {
if (!audioFormats.isNullOrEmpty()){
text = "id: " + audioFormats.joinToString("+") { it.format_id }
visibility = View.VISIBLE
}else if (chosenFormat.vcodec != "none" && chosenFormat.vcodec != "" && chosenFormat.acodec != "none" && chosenFormat.acodec != "") {
text = chosenFormat.acodec
visibility = View.VISIBLE
}else{
visibility = View.GONE
}
@ -1351,12 +1354,18 @@ object UiUtil {
subtitleLanguages.visibility = View.VISIBLE
}
if (items.all { it.videoPreferences.writeAutoSubs}) {
saveAutoSubtitles.isChecked = true
subtitleLanguages.visibility = View.VISIBLE
}
saveSubtitles.setOnCheckedChangeListener { _, _ ->
subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked
subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked || saveAutoSubtitles.isChecked
saveSubtitlesClicked(saveSubtitles.isChecked)
}
saveAutoSubtitles.setOnCheckedChangeListener { _, _ ->
subtitleLanguages.isEnabled = embedSubs.isChecked || saveSubtitles.isChecked || saveAutoSubtitles.isChecked
saveAutoSubtitlesClicked(saveAutoSubtitles.isChecked)
}
@ -1650,6 +1659,7 @@ object UiUtil {
textSize = 20f
}
sheet.findViewById<TextView>(R.id.bottom_sheet_subtitle)?.text = message
sheet.findViewById<LinearLayout>(R.id.multiple_top_info)?.isVisible = false
var adapter: AlreadyExistsAdapter? = null
val list = mutableListOf<Pair<DownloadItem, Long?>>()
@ -1712,7 +1722,7 @@ object UiUtil {
layoutManager = GridLayoutManager(context, 1)
this.adapter = adapter
enableFastScroll()
setPadding(0,20,0,0)
setPadding(0,0,0,0)
}
@ -1993,7 +2003,7 @@ object UiUtil {
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(R.string.cancel)
context.getString(R.string.dismiss)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()

View file

@ -185,7 +185,7 @@ class UpdateUtil(var context: Context) {
.setTitle(activity.getString(R.string.changelog))
.setView(scrollView)
.setIcon(R.drawable.ic_chapters)
.setNegativeButton(context.resources.getString(R.string.cancel)) { _: DialogInterface?, _: Int -> }
.setNegativeButton(context.resources.getString(R.string.dismiss)) { _: DialogInterface?, _: Int -> }
Handler(Looper.getMainLooper()).post {
changeLogDialog.show()
}

View file

@ -88,7 +88,8 @@ class DownloadWorker(
queuedItems.collect { items ->
runningYTDLInstances.clear()
dao.getActiveDownloadsList().forEach {
val activeDownloads = dao.getActiveDownloadsList()
activeDownloads.forEach {
runningYTDLInstances.add(it.id)
}
@ -155,7 +156,7 @@ class DownloadWorker(
runCatching {
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line ->
setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line.chunked(5000).first().toString(), "id" to downloadItem.id))
setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line.chunked(5000).ifEmpty { listOf("") }.first().toString(), "id" to downloadItem.id))
val title: String = downloadItem.title.ifEmpty { downloadItem.url }
notificationUtil.updateDownloadNotification(
downloadItem.id.toInt(),
@ -267,7 +268,7 @@ class DownloadWorker(
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
notificationUtil.createDownloadFinished(
downloadItem.id, downloadItem.title, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else finalPaths, resources
downloadItem.id, downloadItem.title, downloadItem.type, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else finalPaths, resources
)
if (wasQuickDownloaded && createResultItem){
@ -335,7 +336,7 @@ class DownloadWorker(
setProgressAsync(workDataOf("progress" to 100, "output" to it.toString(), "id" to downloadItem.id))
}
if (items.size <= 1) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
//if (items.size <= 1) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
}
}
}

View file

@ -14,6 +14,7 @@ import androidx.work.WorkerParameters
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import java.io.File
import java.nio.file.Files
@ -28,7 +29,8 @@ class MoveCacheFilesWorker(
val notificationUtil = NotificationUtil(App.instance)
val id = System.currentTimeMillis().toInt()
val downloadFolders = File(context.cacheDir.absolutePath + "/downloads")
val cachePath = FileUtil.getCachePath(context)
val downloadFolders = File(cachePath)
val allContent = downloadFolders.walk()
allContent.drop(1)
val totalFiles = allContent.count()
@ -44,7 +46,7 @@ class MoveCacheFilesWorker(
allContent.forEach {
progress++
notificationUtil.updateCacheMovingNotification(id, progress, totalFiles)
val destFile = File(destination.absolutePath + "/${it.absolutePath.removePrefix(context.cacheDir.absolutePath + "/downloads")}")
val destFile = File(destination.absolutePath + "/${it.absolutePath.removePrefix(cachePath)}")
if (it.isDirectory) {
destFile.mkdirs()
return@forEach

View file

@ -64,9 +64,8 @@ class TerminalDownloadWorker(
)
if (sharedPreferences.getBoolean("use_cookies", false)){
val cookiesFile = File(context.cacheDir, "cookies.txt")
if (cookiesFile.exists()){
request.addOption("--cookies", cookiesFile.absolutePath)
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
val useHeader = sharedPreferences.getBoolean("use_header", false)
@ -144,7 +143,6 @@ class TerminalDownloadWorker(
return runBlocking {
if (logDownloads) logRepo.update(it.out, logItem.id)
dao.updateLog(it.out, itemId.toLong())
Thread.sleep(1000)
dao.delete(itemId.toLong())
notificationUtil.cancelDownloadNotification(itemId)
@ -156,7 +154,6 @@ class TerminalDownloadWorker(
if (it.message != null){
if (logDownloads) logRepo.update(it.message!!, logItem.id)
dao.updateLog(it.message!!, itemId.toLong())
Thread.sleep(1000)
dao.delete(itemId.toLong())
}
}

View file

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.recyclerview.widget.RecyclerView android:id="@+id/download_recyclerview"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
</androidx.recyclerview.widget.RecyclerView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,66 +1,216 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/already_exists_constraint"
<com.google.android.material.card.MaterialCardView android:id="@+id/download_card_view"
android:layout_width="match_parent"
android:layout_marginBottom="10dp"
android:layout_height="wrap_content"
android:backgroundTint="@android:color/transparent"
android:checkable="true"
android:paddingHorizontal="20dp"
android:paddingBottom="5dp"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:layout_height="wrap_content">
app:checkedIcon="@null"
app:strokeColor="?attr/colorPrimary"
app:cardPreventCornerOverlap="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
app:strokeWidth="0dp"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:layout_height="wrap_content">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/downloads_image_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:background="?attr/colorSurfaceVariant"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toStartOf="@+id/download_item_data"
app:layout_constraintHorizontal_weight="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" />
<Button
android:id="@+id/already_exists_title"
style="@style/Widget.Material3.Button.TonalButton.Icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:cornerRadius="10dp"
android:autoLink="all"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginEnd="10dp"
app:layout_constraintEnd_toStartOf="@+id/already_exists_buttons"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/already_exists_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/already_exists_edit"
style="@style/Widget.Material3.Button.IconButton.Filled"
<TextView
android:id="@+id/duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_edit"
android:contentDescription="@string/edit"
app:layout_constraintBottom_toBottomOf="@id/already_exists_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/already_exists_title"
app:layout_constraintVertical_bias="1.0" />
android:layout_margin="5dp"
android:background="#80000000"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="@+id/downloads_image_view"
app:layout_constraintEnd_toEndOf="@+id/downloads_image_view" />
<Button
android:id="@+id/already_exists_delete"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:contentDescription="@string/Remove"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/download_item_data"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_delete_24"
app:layout_constraintBottom_toBottomOf="@id/already_exists_title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/already_exists_title" />
app:layout_constraintHorizontal_weight="0.7"
app:layout_constraintStart_toEndOf="@+id/downloads_image_view"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
</LinearLayout>
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="3"
app:layout_constraintVertical_bias="0.0"
android:paddingHorizontal="5dp"
android:scrollbars="none"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/horizontalScrollView"
app:layout_constraintEnd_toStartOf="@+id/already_exists_buttons"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="title,already_exists_buttons" />
<HorizontalScrollView
android:id="@+id/horizontalScrollView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="bottom"
android:paddingStart="5dp"
android:paddingEnd="0dp"
android:scrollbars="none"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/barrier">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/format_note"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/codec"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/file_size"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</HorizontalScrollView>
<LinearLayout
android:id="@+id/already_exists_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<Button
android:id="@+id/already_exists_edit"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_edit"
android:contentDescription="@string/edit"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="1.0" />
<Button
android:id="@+id/already_exists_delete"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:contentDescription="@string/Remove"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_delete_24"
app:layout_constraintEnd_toEndOf="parent" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -92,6 +92,7 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/multiple_top_info"
android:layout_width="match_parent"
android:layout_height="wrap_content">

View file

@ -92,7 +92,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:clickable="false"
android:focusable="false"
android:scrollbars="none">
@ -182,6 +181,7 @@
android:ellipsize="end"
android:gravity="bottom|end"
android:maxLength="10"
android:layout_marginEnd="5dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="@+id/format_note"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -96,13 +96,13 @@
android:layout_marginEnd="4dp"
android:background="@color/light_grey"
android:clickable="false"
android:gravity="bottom|end"
android:maxLength="10"
android:layout_marginTop="10dp"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -115,18 +115,26 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:id="@+id/recyclerviewhistorys"
android:orientation="vertical"
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
android:paddingBottom="100dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager">
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
</androidx.recyclerview.widget.RecyclerView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerviewhistorys"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:orientation="vertical"
android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
<include layout="@layout/no_results"
android:visibility="gone" />

View file

@ -1,121 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/playlist_card_constraintLayout"
<com.google.android.material.card.MaterialCardView android:id="@+id/playlist_card"
android:layout_width="match_parent"
android:layout_marginBottom="10dp"
android:layout_height="wrap_content"
android:checkable="true"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:layout_height="wrap_content">
app:strokeWidth="0dp"
android:backgroundTint="@android:color/transparent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/download_card_view"
android:layout_width="0dp"
android:paddingHorizontal="10dp"
android:paddingVertical="3dp"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardPreventCornerOverlap="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/checkBox"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:strokeWidth="0dp">
android:paddingHorizontal="20dp"
android:paddingVertical="10dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/downloads_image_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:background="?attr/colorSurfaceVariant"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toStartOf="@+id/download_item_data"
app:layout_constraintHorizontal_weight="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" />
<TextView
android:id="@+id/duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#80000000"
android:clickable="false"
android:layout_margin="5dp"
android:ellipsize="end"
android:gravity="center"
android:textStyle="bold"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textColor="@color/white"
android:textSize="12sp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="@+id/downloads_image_view"
app:layout_constraintEnd_toEndOf="@+id/downloads_image_view" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/downloads_image_view"
android:clickable="false"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHorizontal_weight="0.4"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toStartOf="@+id/download_item_data"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
/>
android:id="@+id/download_item_data"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/checkBox"
app:layout_constraintHorizontal_weight="0.7"
app:layout_constraintStart_toEndOf="@+id/downloads_image_view"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#80000000"
android:clickable="false"
android:layout_margin="5dp"
android:ellipsize="end"
android:gravity="center"
android:textStyle="bold"
android:maxLength="17"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textColor="@color/white"
android:textSize="12sp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="@+id/downloads_image_view"
app:layout_constraintEnd_toEndOf="@+id/downloads_image_view" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/download_item_data"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHorizontal_weight="0.7"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:paddingHorizontal="10dp"
android:scrollbars="none"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/downloads_image_view"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:paddingHorizontal="10dp"
android:scrollbars="none"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<TextView
android:id="@+id/author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingHorizontal="10dp"
android:scrollbars="none"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingHorizontal="10dp"
android:scrollbars="none"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
<CheckBox
android:id="@+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<CheckBox
android:id="@+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
@ -19,16 +18,16 @@
android:id="@+id/bottomAppBar"
style="@style/Widget.Material3.BottomAppBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="70dp"
app:menu="@menu/select_playlist_menu"
android:layout_gravity="bottom">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/from_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginStart="50dp"
android:layout_marginEnd="10dp"
android:hint="@string/start">
@ -41,7 +40,7 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/to_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:hint="@string/end"
@ -58,12 +57,20 @@
</com.google.android.material.bottomappbar.BottomAppBar>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/check_all"
android:layout_width="wrap_content"
app:icon="@drawable/ic_select_all"
android:layout_height="wrap_content"
app:layout_anchor="@id/bottomAppBar"/>
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Primary"
android:layout_width="55dp"
app:elevation="0dp"
android:elevation="0dp"
app:borderWidth="0dp"
android:contentDescription="@string/select_all"
android:layout_height="55dp"
app:layout_anchor="@id/bottomAppBar"
android:layout_marginHorizontal="5dp"
android:src="@drawable/ic_select_all"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/download_video" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@ -79,44 +86,45 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:paddingTop="20dp">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_height="wrap_content"
app:elevation="0dp">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
app:menu="@menu/playlist_download_menu"
android:layout_height="?attr/actionBarSize"
android:paddingEnd="16dp"
android:paddingStart="6dp"
app:contentInsetStartWithNavigation="0dp"
app:navigationIcon="@drawable/baseline_close_24" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
<TextView
android:id="@+id/bottom_sheet_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginHorizontal="20dp"
android:maxLines="2"
android:singleLine="false"
android:text="@string/select"
android:textSize="25sp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_ok_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/count"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/configure_download"
android:textSize="12sp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_ok_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_title" />
<Button
android:id="@+id/bottomsheet_ok_button"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/ok"
app:icon="@drawable/ic_check"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appbar"
app:layout_constraintVertical_bias="0.0">
</LinearLayout>
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@ -124,9 +132,8 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloadMultipleRecyclerview"
android:layout_width="match_parent"
tools:listitem="@layout/playlist_item"
android:paddingTop="10dp"
android:layout_height="wrap_content"
android:focusableInTouchMode="true"
android:orientation="vertical"
android:paddingBottom="80dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

View file

@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity" >
<item
android:id="@+id/select_between"
android:visible="false"
android:title="@string/select_between"
android:icon="@drawable/ic_select_between"
app:showAsAction="ifRoom" />
<item
android:id="@+id/delete_results"
android:title="@string/remove_results"

View file

@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity" >
<item
android:id="@+id/select_between"
android:visible="false"
android:title="@string/select_between"
android:icon="@drawable/ic_select_between"
app:showAsAction="ifRoom" />
<item
android:id="@+id/delete_results"
android:title="@string/remove_results"

View file

@ -3,6 +3,13 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:theme="@style/BaseTheme">
<item
android:id="@+id/select_between"
android:visible="false"
android:title="@string/select_between"
android:icon="@drawable/ic_select_between"
app:showAsAction="never" />
<item android:title="@string/invert_selected"
android:icon="@drawable/ic_terminal"
app:showAsAction="never"

View file

@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity" >
<item
android:id="@+id/select_between"
android:visible="false"
android:title="@string/select_between"
android:icon="@drawable/ic_select_between"
app:showAsAction="ifRoom" />
<item
android:id="@+id/delete_results"
android:title="@string/remove_results"

View file

@ -375,4 +375,6 @@
<string name="all_queries_finished">All Queries have been processed</string>
<string name="select_between">Select Items Between</string>
<string name="reset_processed_links">Reset Already Processed Links</string>
<string name="no_download_fragments">Don\'t download as Fragments</string>
<string name="no_download_fragments_summary">Don\'t use .part files and write directly into output file</string>
</resources>

View file

@ -36,6 +36,15 @@
app:title="@string/cache_first" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_save_alt_24"
android:key="no_part"
app:summary="@string/no_download_fragments_summary"
app:title="@string/no_download_fragments" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"

View file

@ -2,241 +2,249 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_money"
app:key="use_sponsorblock"
app:title="@string/use_sponsorblock" />
<PreferenceCategory android:title="@string/general">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_money"
app:key="use_sponsorblock"
app:title="@string/use_sponsorblock" />
<MultiSelectListPreference
app:defaultValue="@array/sponsorblock_settings_default"
app:icon="@drawable/ic_money"
android:dependency="use_sponsorblock"
app:dialogTitle="@string/select_sponsorblock_filtering"
app:entries="@array/sponsorblock_settings_entries"
app:entryValues="@array/sponsorblock_settings_values"
app:key="sponsorblock_filters"
app:summary="@string/select_sponsorblock_filtering"
app:title="SponsorBlock" />
<MultiSelectListPreference
app:defaultValue="@array/sponsorblock_settings_default"
app:icon="@drawable/ic_money"
android:dependency="use_sponsorblock"
app:dialogTitle="@string/select_sponsorblock_filtering"
app:entries="@array/sponsorblock_settings_entries"
app:entryValues="@array/sponsorblock_settings_values"
app:key="sponsorblock_filters"
app:summary="@string/select_sponsorblock_filtering"
app:title="SponsorBlock" />
<EditTextPreference
android:dependency="use_sponsorblock"
app:icon="@drawable/ic_money"
app:key="sponsorblock_url"
app:useSimpleSummaryProvider="true"
app:defaultValue=""
app:title="@string/sponsorblock_api_url" />
<EditTextPreference
android:dependency="use_sponsorblock"
app:icon="@drawable/ic_money"
app:key="sponsorblock_url"
app:useSimpleSummaryProvider="true"
app:defaultValue=""
app:title="@string/sponsorblock_api_url" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_clock"
app:key="mtime"
app:summary="@string/enable_mtime_summary"
app:title="@string/enable_mtime" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_clock"
app:key="mtime"
app:summary="@string/enable_mtime_summary"
app:title="@string/enable_mtime" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/baseline_video_metadata"
app:key="embed_metadata"
app:summary="@string/embed_metadata_summary"
app:title="@string/embed_metadata" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/baseline_description_24"
app:key="write_description"
android:summary="@string/write_description_summary"
app:title="@string/write_description" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_subtitles"
app:key="embed_subtitles"
app:summary="@string/embed_subs_summary"
app:title="@string/embed_subtitles" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_terminal"
app:key="use_extra_commands"
android:summary="@string/use_extra_commands_summary"
app:title="@string/use_extra_commands" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_subtitles_alternative"
app:key="write_subtitles"
app:summary="@string/save_subs_desc"
app:title="@string/save_subs" />
</PreferenceCategory>
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_subtitles_alternative"
app:key="write_auto_subtitles"
app:summary="@string/save_subs_desc"
app:title="@string/save_auto_subs" />
<PreferenceCategory android:title="@string/audio">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/baseline_video_metadata"
app:key="embed_metadata"
app:summary="@string/embed_metadata_summary"
app:title="@string/embed_metadata" />
<Preference
android:icon="@drawable/baseline_subject_24"
app:key="subs_lang"
app:useSimpleSummaryProvider="true"
app:defaultValue="en.*,.*-orig"
app:title="@string/subtitle_languages" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_image"
app:key="embed_thumbnail"
app:summary="@string/embed_thumb_summary"
app:title="@string/embed_thumb" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:dependency="embed_thumbnail"
app:icon="@drawable/ic_cut"
app:key="crop_thumbnail"
app:summary="@string/crop_thumb_summary"
app:title="@string/crop_thumb" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_cut"
app:key="force_keyframes"
app:summary="@string/force_keyframes_summary"
app:title="@string/force_keyframes" />
</PreferenceCategory>
<ListPreference
android:defaultValue="srt"
android:entries="@array/sub_formats"
android:entryValues="@array/sub_formats_values"
android:icon="@drawable/baseline_format_quote_24"
app:key="sub_format"
app:useSimpleSummaryProvider="true"
app:title="@string/subtitle_format" />
<PreferenceCategory android:title="@string/video">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_subtitles"
app:key="embed_subtitles"
app:summary="@string/embed_subs_summary"
app:title="@string/embed_subtitles" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/baseline_description_24"
app:key="write_description"
android:summary="@string/write_description_summary"
app:title="@string/write_description" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_subtitles_alternative"
app:key="write_subtitles"
app:summary="@string/save_subs_desc"
app:title="@string/save_subs" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_image"
app:key="embed_thumbnail"
app:summary="@string/embed_thumb_summary"
app:title="@string/embed_thumb" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_subtitles_alternative"
app:key="write_auto_subtitles"
app:summary="@string/save_subs_desc"
app:title="@string/save_auto_subs" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:dependency="embed_thumbnail"
app:icon="@drawable/ic_cut"
app:key="crop_thumbnail"
app:summary="@string/crop_thumb_summary"
app:title="@string/crop_thumb" />
<Preference
android:icon="@drawable/baseline_subject_24"
app:key="subs_lang"
app:useSimpleSummaryProvider="true"
app:defaultValue="en.*,.*-orig"
app:title="@string/subtitle_languages" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_cut"
app:key="force_keyframes"
app:summary="@string/force_keyframes_summary"
app:title="@string/force_keyframes" />
<ListPreference
android:defaultValue="srt"
android:entries="@array/sub_formats"
android:entryValues="@array/sub_formats_values"
android:icon="@drawable/baseline_format_quote_24"
app:key="sub_format"
app:useSimpleSummaryProvider="true"
app:title="@string/subtitle_format" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_image"
app:key="write_thumbnail"
app:summary="@string/save_thumb_summary"
app:title="@string/save_thumb" />
<ListPreference
android:defaultValue="png"
android:entries="@array/thumbnail_containers"
android:entryValues="@array/thumbnail_containers_values"
android:icon="@drawable/ic_image"
app:key="thumbnail_format"
app:useSimpleSummaryProvider="true"
app:title="@string/thumbnail_format" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_chapters"
app:key="add_chapters"
app:summary="@string/add_chapters_summary"
app:title="@string/add_chapters" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/format">
<ListPreference
android:defaultValue="Default"
android:entries="@array/audio_containers"
android:entryValues="@array/audio_containers_values"
android:icon="@drawable/ic_code"
app:key="audio_format"
app:useSimpleSummaryProvider="true"
app:title="@string/audio_format" />
<ListPreference
android:defaultValue="Default"
android:entries="@array/video_containers"
android:entryValues="@array/video_containers_values"
android:icon="@drawable/ic_code"
app:key="video_format"
app:useSimpleSummaryProvider="true"
app:title="@string/video_format" />
<ListPreference
android:defaultValue="avc|h264"
android:entries="@array/video_codec"
android:entryValues="@array/video_codec_values"
android:icon="@drawable/ic_code"
app:useSimpleSummaryProvider="true"
app:key="video_codec"
app:title="@string/preferred_video_codec" />
<ListPreference
android:defaultValue="m4a|mp4a|aac"
android:entries="@array/audio_codec"
android:entryValues="@array/audio_codec_values"
android:icon="@drawable/ic_code"
app:useSimpleSummaryProvider="true"
app:key="audio_codec"
app:title="@string/preferred_audio_codec" />
<ListPreference
app:icon="@drawable/ic_language"
app:dialogTitle="@string/preferred_audio_language"
app:entries="@array/language_names"
app:entryValues="@array/language_codes"
app:key="audio_language"
app:useSimpleSummaryProvider="true"
app:title="@string/preferred_audio_language" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_music"
app:key="use_audio_quality"
app:title="@string/custom_audio_quality" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_image"
app:key="write_thumbnail"
app:summary="@string/save_thumb_summary"
app:title="@string/save_thumb" />
<SeekBarPreference
android:defaultValue="0"
android:max="10"
android:dependency="use_audio_quality"
app:key="audio_quality"
app:min="0"
app:showSeekBarValue="true"
app:summary="@string/audio_quality_summary"
app:title="@string/audio_quality" />
<ListPreference
android:defaultValue="png"
android:entries="@array/thumbnail_containers"
android:entryValues="@array/thumbnail_containers_values"
android:icon="@drawable/ic_image"
app:key="thumbnail_format"
app:useSimpleSummaryProvider="true"
app:title="@string/thumbnail_format" />
<ListPreference
android:defaultValue="best"
android:entries="@array/video_formats"
android:entryValues="@array/video_formats_values"
android:icon="@drawable/ic_video"
app:key="video_quality"
app:useSimpleSummaryProvider="true"
app:title="@string/video_quality" />
<EditTextPreference
app:key="format_id"
app:defaultValue=""
app:summary="@string/preferred_format_id_summary"
app:title="@string/preferred_format_id" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_chapters"
app:key="add_chapters"
app:summary="@string/add_chapters_summary"
app:title="@string/add_chapters" />
<ListPreference
android:defaultValue="Default"
android:entries="@array/audio_containers"
android:entryValues="@array/audio_containers_values"
android:icon="@drawable/ic_code"
app:key="audio_format"
app:useSimpleSummaryProvider="true"
app:title="@string/audio_format" />
<ListPreference
android:defaultValue="Default"
android:entries="@array/video_containers"
android:entryValues="@array/video_containers_values"
android:icon="@drawable/ic_code"
app:key="video_format"
app:useSimpleSummaryProvider="true"
app:title="@string/video_format" />
<ListPreference
android:defaultValue="avc|h264"
android:entries="@array/video_codec"
android:entryValues="@array/video_codec_values"
android:icon="@drawable/ic_code"
app:useSimpleSummaryProvider="true"
app:key="video_codec"
app:title="@string/preferred_video_codec" />
<ListPreference
android:defaultValue="m4a|mp4a|aac"
android:entries="@array/audio_codec"
android:entryValues="@array/audio_codec_values"
android:icon="@drawable/ic_code"
app:useSimpleSummaryProvider="true"
app:key="audio_codec"
app:title="@string/preferred_audio_codec" />
<ListPreference
app:icon="@drawable/ic_language"
app:dialogTitle="@string/preferred_audio_language"
app:entries="@array/language_names"
app:entryValues="@array/language_codes"
app:key="audio_language"
app:useSimpleSummaryProvider="true"
app:title="@string/preferred_audio_language" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_music"
app:key="use_audio_quality"
app:title="@string/custom_audio_quality" />
<SeekBarPreference
android:defaultValue="0"
android:max="10"
android:dependency="use_audio_quality"
app:key="audio_quality"
app:min="0"
app:showSeekBarValue="true"
app:summary="@string/audio_quality_summary"
app:title="@string/audio_quality" />
<ListPreference
android:defaultValue="best"
android:entries="@array/video_formats"
android:entryValues="@array/video_formats_values"
android:icon="@drawable/ic_video"
app:key="video_quality"
app:useSimpleSummaryProvider="true"
app:title="@string/video_quality" />
<EditTextPreference
app:key="format_id"
app:defaultValue=""
app:summary="@string/preferred_format_id_summary"
app:title="@string/preferred_format_id" />
<EditTextPreference
app:key="format_id_audio"
app:defaultValue=""
app:summary="@string/preferred_format_id_summary"
app:title="@string/preferred_format_id" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_terminal"
app:key="use_extra_commands"
android:summary="@string/use_extra_commands_summary"
app:title="@string/use_extra_commands" />
<EditTextPreference
app:key="format_id_audio"
app:defaultValue=""
app:summary="@string/preferred_format_id_summary"
app:title="@string/preferred_format_id" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -34,7 +34,7 @@
app:title="@string/auto_update_ytdlp" />
<ListPreference
android:defaultValue="piped"
android:defaultValue="yt-dlp"
android:entries="@array/formats_source"
android:entryValues="@array/formats_source_values"
android:icon="@drawable/baseline_manage_search_24"

View file

@ -22,8 +22,8 @@ buildscript {
coroutineVer = '1.7.3'
retrofitVer = "2.9.0"
kodeinVer = "7.16.0"
navVer = "2.7.6"
media3_version = "1.2.0"
navVer = "2.7.7"
media3_version = "1.2.1"
agp_version = '8.1.0'
agp_version1 = '7.4.2'
roomVer = '2.5.2'