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 properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 7 def versionMinor = 7
def versionPatch = 3 def versionPatch = 4
def versionBuild = 1 // bump for dogfood builds, public betas, etc. def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def versionExt = ".${versionBuild}" def versionExt = ".${versionBuild}"
//if (versionBuild > 0){ if (versionBuild > 0){
// versionExt = ".${versionBuild}-beta" versionExt = ".${versionBuild}-beta"
//} }
android { android {
namespace 'com.deniscerri.ytdlnis' namespace 'com.deniscerri.ytdlnis'
@ -144,7 +144,7 @@ dependencies {
implementation "androidx.navigation:navigation-ui-ktx:$navVer" implementation "androidx.navigation:navigation-ui-ktx:$navVer"
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.test.ext:junit-ktx:1.1.5' 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" testImplementation "junit:junit:$junitVer"
androidTestImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer"
androidTestImplementation "androidx.test.ext:junit:$androidJunitVer" androidTestImplementation "androidx.test.ext:junit:$androidJunitVer"
@ -168,7 +168,7 @@ dependencies {
implementation 'androidx.paging:paging-runtime-ktx:3.2.1' implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation "androidx.room:room-paging:$roomVer" implementation "androidx.room:room-paging:$roomVer"
androidTestImplementation "androidx.room:room-testing:$roomVer" androidTestImplementation "androidx.room:room-testing:$roomVer"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation "androidx.compose.runtime:runtime:$composeVer" implementation "androidx.compose.runtime:runtime:$composeVer"
androidTestImplementation 'com.google.truth:truth:1.1.5' androidTestImplementation 'com.google.truth:truth:1.1.5'
@ -180,7 +180,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4' implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0"
implementation "com.github.Irineu333:Highlight-KT:1.0.4" implementation "com.github.Irineu333:Highlight-KT:1.0.4"
// For media playback using ExoPlayer // For media playback using ExoPlayer
@ -196,6 +196,6 @@ dependencies {
implementation "com.anggrayudi:storage:1.5.5" implementation "com.anggrayudi:storage:1.5.5"
implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'me.zhanghai.android.fastscroll:library:1.3.0'
implementation 'com.google.accompanist:accompanist-webview:0.30.1' implementation 'com.google.accompanist:accompanist-webview:0.30.1'
implementation 'androidx.compose.material3:material3-android:1.2.0-beta01' implementation 'androidx.compose.material3:material3-android:1.2.0'
implementation "io.noties.markwon:core:4.6.2" 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.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <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_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <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" /> tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" /> <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.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" <uses-permission
android:maxSdkVersion="32"/> android:name="android.permission.SCHEDULE_EXACT_ALARM"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" /> <uses-permission android:name="android.permission.USE_EXACT_ALARM" />
@ -71,6 +75,152 @@
<data android:mimeType="video/*" /> <data android:mimeType="video/*" />
<data android:mimeType="audio/*" /> <data android:mimeType="audio/*" />
</intent-filter> </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>
<activity <activity
android:name=".receiver.ShareFileActivity" android:name=".receiver.ShareFileActivity"

View file

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

View file

@ -11,6 +11,8 @@ import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.View 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.BaseActivity
import com.deniscerri.ytdlnis.ui.HomeFragment import com.deniscerri.ytdlnis.ui.HomeFragment
import com.deniscerri.ytdlnis.ui.more.settings.SettingsActivity 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.FileUtil
import com.deniscerri.ytdlnis.util.ThemeUtil import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UpdateUtil import com.deniscerri.ytdlnis.util.UpdateUtil
@ -56,6 +59,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -81,6 +85,7 @@ class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
CrashListener(this).registerExceptionHandler()
ThemeUtil.updateTheme(this) ThemeUtil.updateTheme(this)
window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this) window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
@ -90,7 +95,11 @@ class MainActivity : BaseActivity() {
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
preferences = PreferenceManager.getDefaultSharedPreferences(context) preferences = PreferenceManager.getDefaultSharedPreferences(context)
if (preferences.getBoolean("incognito", false)) resultViewModel.deleteAll() if (preferences.getBoolean("incognito", false)) {
lifecycleScope.launch(Dispatchers.IO){
resultViewModel.deleteAll()
}
}
askPermissions() askPermissions()
checkUpdate() checkUpdate()
@ -211,13 +220,15 @@ class MainActivity : BaseActivity() {
} }
navController.addOnDestinationChangedListener { _, destination, _ -> navController.addOnDestinationChangedListener { _, destination, _ ->
if (navigationView is NavigationBarView){ Handler(Looper.getMainLooper()).post {
when(destination.id){ if (navigationView is NavigationBarView){
R.id.homeFragment, R.id.historyFragment, R.id.moreFragment -> showBottomNavigation() when(destination.id){
else -> hideBottomNavigation() R.id.homeFragment, R.id.historyFragment, R.id.moreFragment -> showBottomNavigation()
else -> hideBottomNavigation()
}
} }
} }
} }
navigationView.visibilityChanged { navigationView.visibilityChanged {
@ -288,9 +299,16 @@ class MainActivity : BaseActivity() {
findViewById<FragmentContainerView>(R.id.frame_layout).updateLayoutParams { findViewById<FragmentContainerView>(R.id.frame_layout).updateLayoutParams {
this.width = LayoutParams.MATCH_PARENT this.width = LayoutParams.MATCH_PARENT
} }
navigationView.animate()?.translationX(-navigationView.width.toFloat())?.setDuration(300)?.withEndAction {
navigationView.visibility = View.GONE if (resources.getBoolean(R.bool.is_right_to_left)){
}?.start() 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> fun getProcessingDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Active'") @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')") @Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused','PausedReQueued')")
fun getActiveAndQueuedDownloadsList() : List<DownloadItem> 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") @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> 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 @Dao
interface ResultDao { interface ResultDao {
@Query("SELECT * FROM results") @Query("SELECT * FROM results order by id")
fun getResults() : Flow<List<ResultItem>> fun getResults() : Flow<List<ResultItem>>
@Query("SELECT COUNT(id) FROM results") @Query("SELECT COUNT(id) FROM results")
@ -49,4 +49,7 @@ interface ResultDao {
@Query("SELECT * FROM results WHERE url=:url LIMIT 1") @Query("SELECT * FROM results WHERE url=:url LIMIT 1")
fun getResultByURL(url: String) : ResultItem? 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 fun getActiveTerminalsCount() : Int
@Query("UPDATE terminalDownloads set log=:l where id=:id") @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") @Query("SELECT * FROM terminalDownloads WHERE id=:id LIMIT 1")
fun getTerminalById(id: Long) : TerminalItem? fun getTerminalById(id: Long) : TerminalItem?
@Transaction @Transaction
fun updateLog(line: String, id: Long){ suspend fun updateLog(line: String, id: Long){
val t = getTerminalById(id) ?: return val t = getTerminalById(id) ?: return
val log = t.log ?: "" val log = t.log ?: ""
updateTerminalLog(log.appendLineToLog(line), id) updateTerminalLog(log.appendLineToLog(line), id)

View file

@ -46,6 +46,26 @@ class HistoryRepository(private val historyDao: HistoryDao) {
return filtered 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){ suspend fun insert(item: HistoryItem){
historyDao.insert(item) historyDao.insert(item)
} }

View file

@ -145,7 +145,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
search(inputQuery, resetResults, addToResults) search(inputQuery, resetResults, addToResults)
} }
SourceType.YT_DLP -> { 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 package com.deniscerri.ytdlnis.database.viewmodel
import android.R.attr.description
import android.R.attr.duration
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteDatabase.OPEN_READONLY import android.database.sqlite.SQLiteDatabase.OPEN_READONLY
import android.database.sqlite.SQLiteDatabase.OPEN_READWRITE
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import android.webkit.CookieManager 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.models.CookieItem
import com.deniscerri.ytdlnis.database.repository.CookieRepository import com.deniscerri.ytdlnis.database.repository.CookieRepository
import com.deniscerri.ytdlnis.ui.more.WebViewActivity 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.net.URL
import java.util.Date import java.util.Date
class CookieViewModel(private val application: Application) : AndroidViewModel(application) { class CookieViewModel(private val application: Application) : AndroidViewModel(application) {
private val repository: CookieRepository private val repository: CookieRepository
val items: LiveData<List<CookieItem>> val items: LiveData<List<CookieItem>>
@ -33,6 +39,15 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
"# WebView Generated by the YTDLnis app\n" + "# WebView Generated by the YTDLnis app\n" +
"# This is a generated file! Do not edit." "# 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 { init {
val dao = DBManager.getInstance(application).cookieDao val dao = DBManager.getInstance(application).cookieDao
repository = CookieRepository(dao) repository = CookieRepository(dao)
@ -86,14 +101,7 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
dbPath.absolutePath, null, OPEN_READONLY 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>() val cookieList = mutableListOf<WebViewActivity.CookieItem>()
db.query( db.query(
"cookies", projection, null, null, null, null, null "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 host = if (hostKey[0] != '.') ".$hostKey" else hostKey
cookieList.add(
val requiredHost = URL(url).host WebViewActivity.CookieItem(
if(host.contains(requiredHost)){ domain = host,
cookieList.add( name = name,
WebViewActivity.CookieItem( value = value,
domain = host, path = path,
name = name, secure = secure,
value = value, expiry = expiry
path = path,
secure = secure,
expiry = expiry
)
) )
} )
} }
close() close()
} }
@ -134,14 +138,17 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
fun updateCookiesFile() = viewModelScope.launch(Dispatchers.IO) { fun updateCookiesFile() = viewModelScope.launch(Dispatchers.IO) {
val cookies = repository.getAll() val cookies = repository.getAll()
val cookieTXT = StringBuilder(cookieHeader) val cookieTXT = StringBuilder(cookieHeader)
val cookieFile = File(application.cacheDir, "cookies.txt") FileUtil.getCookieFile(application, true){ c ->
if (cookies.isEmpty()) cookieFile.apply { writeText("") } val cookieFile = File(c)
cookies.forEach { if (cookies.isEmpty()) cookieFile.apply { writeText("") }
it.content.lines().forEach {line -> cookies.forEach {
if (! cookieTXT.contains(line)) cookieTXT.append(it.content) 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() { suspend fun importFromClipboard() {
@ -170,11 +177,14 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
try{ try{
val clipboard: ClipboardManager = val clipboard: ClipboardManager =
application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val cookieFile = File(application.cacheDir, "cookies.txt") FileUtil.getCookieFile(application, true){c ->
if (! cookieFile.exists()) updateCookiesFile() val cookieFile = File(c)
cookieFile.readText().let { if (! cookieFile.exists()) updateCookiesFile()
clipboard.setText(it) cookieFile.readText().let {
clipboard.setText(it)
}
} }
}catch (e: Exception){ }catch (e: Exception){
e.printStackTrace() e.printStackTrace()
} }
@ -182,12 +192,14 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
fun exportToFile(exported: (File?) -> Unit) { fun exportToFile(exported: (File?) -> Unit) {
try{ try{
val cookieFile = File(application.cacheDir, "cookies.txt") FileUtil.getCookieFile(application, true){ c ->
if (!cookieFile.exists()) updateCookiesFile() val cookieFile = File(c)
if (!cookieFile.exists()) updateCookiesFile()
val downloads = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "YTDLnis_Cookies.txt") val downloads = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "YTDLnis_Cookies.txt")
val file = cookieFile.copyTo(downloads, true) val file = cookieFile.copyTo(downloads, true)
exported(file) exported(file)
}
}catch (e: Exception){ }catch (e: Exception){
e.printStackTrace() e.printStackTrace()
exported(null) exported(null)

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.database.viewmodel package com.deniscerri.ytdlnis.database.viewmodel
import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
@ -15,6 +16,7 @@ import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
@ -179,10 +181,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
if (sharedPreferences.getBoolean("incognito", false)){ if (sharedPreferences.getBoolean("incognito", false)){
if (item.status == DownloadRepository.Status.Cancelled.toString() || item.status == DownloadRepository.Status.Error.toString()){ if (item.status == DownloadRepository.Status.Cancelled.toString() || item.status == DownloadRepository.Status.Error.toString()){
repository.delete(item.id) repository.delete(item.id)
return
} }
}else{
repository.update(item)
} }
repository.update(item)
} }
fun getItemByID(id: Long) : DownloadItem { fun getItemByID(id: Long) : DownloadItem {
@ -461,7 +464,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
sharedPreferences.getString("audio_language", "")?.apply { sharedPreferences.getString("audio_language", "")?.apply {
if (this.isNotBlank()){ 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 -> 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 -> 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 -> "^(${videoCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)}
requirements.add { it: Format -> it.acodec == "none" || it.acodec == "" }
return requirements return requirements
} }
@ -948,5 +952,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dao.getIDsBetweenTwoItems(item1, item2, statuses) 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() 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.App
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager 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.Format
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.models.SearchHistoryItem import com.deniscerri.ytdlnis.database.models.SearchHistoryItem
@ -37,6 +39,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
val items : LiveData<List<ResultItem>> val items : LiveData<List<ResultItem>>
private val infoUtil: InfoUtil private val infoUtil: InfoUtil
private val notificationUtil: NotificationUtil private val notificationUtil: NotificationUtil
private val dao: ResultDao
data class ResultsUiState( data class ResultsUiState(
var processing: Boolean, var processing: Boolean,
var errorMessage: Pair<Int, String>?, var errorMessage: Pair<Int, String>?,
@ -67,7 +70,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
init { init {
val dao = DBManager.getInstance(application).resultDao dao = DBManager.getInstance(application).resultDao
repository = ResultRepository(dao, getApplication<Application>().applicationContext) repository = ResultRepository(dao, getApplication<Application>().applicationContext)
searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao) searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao)
items = repository.allResults.asLiveData() 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() repository.deleteAll()
} }
@ -219,7 +222,8 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
updatingData.emit(false) updatingData.emit(false)
updatingData.value = 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.emit(false)
updatingData.value = false updatingData.value = false
updateResultData.emit(null) updateResultData.emit(null)
updateResultData.value = null
} }
suspend fun cancelUpdateFormatsItemData(){ suspend fun cancelUpdateFormatsItemData(){
updateFormatsResultDataJob?.cancel(CancellationException())
updatingFormats.emit(false) updatingFormats.emit(false)
updatingFormats.value = false
updateFormatsResultData.emit(null) updateFormatsResultData.emit(null)
updateFormatsResultData.value = null
updateFormatsResultDataJob?.cancel(CancellationException())
} }
suspend fun updateFormatItemData(result: ResultItem){ suspend fun updateFormatItemData(result: ResultItem){
if (updateFormatsResultDataJob == null || updateFormatsResultDataJob?.isCancelled == true || updateFormatsResultDataJob?.isCompleted == true) { if (updateFormatsResultDataJob == null || updateFormatsResultDataJob?.isCancelled == true || updateFormatsResultDataJob?.isCompleted == true) {
updateFormatsResultDataJob = viewModelScope.launch(Dispatchers.IO) { updateFormatsResultDataJob = viewModelScope.launch(Dispatchers.IO) {
updatingFormats.emit(true) updatingFormats.emit(true)
val formats = kotlin.runCatching { try {
infoUtil.getFormats(result.url) val formats = infoUtil.getFormats(result.url)
}.onFailure { e -> updatingFormats.emit(false)
if (e !is kotlinx.coroutines.CancellationException){ formats.apply {
uiState.update {it.copy( if (formats.isNotEmpty() && updateFormatsResultDataJob?.isCancelled == false) {
processing = false, getItemByURL(result.url)?.apply {
errorMessage = Pair(R.string.no_results, e.message.toString()), this.formats = formats.toMutableList()
actions = mutableListOf(Pair(R.string.copy_log, ResultAction.COPY_LOG)) 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()) 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?.start()
updateFormatsResultDataJob?.invokeOnCompletion { updateFormatsResultDataJob?.invokeOnCompletion {
if (it != null){ if (it != null){
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
updatingFormats.emit(false) 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.ResultItem
import com.deniscerri.ytdlnis.database.models.SearchSuggestionItem import com.deniscerri.ytdlnis.database.models.SearchSuggestionItem
import com.deniscerri.ytdlnis.database.models.SearchSuggestionType 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.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.ui.adapter.HomeAdapter import com.deniscerri.ytdlnis.ui.adapter.HomeAdapter
import com.deniscerri.ytdlnis.ui.adapter.SearchSuggestionsAdapter import com.deniscerri.ytdlnis.ui.adapter.SearchSuggestionsAdapter
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll 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.InfoUtil
import com.deniscerri.ytdlnis.util.ThemeUtil import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
@ -99,7 +101,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
private var searchSuggestionsRecyclerView: RecyclerView? = null private var searchSuggestionsRecyclerView: RecyclerView? = null
private var uiHandler: Handler? = null private var uiHandler: Handler? = null
private var resultsList: List<ResultItem?>? = 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 quickLaunchSheet = false
private var sharedPreferences: SharedPreferences? = null private var sharedPreferences: SharedPreferences? = null
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
@ -115,6 +117,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
mainActivity = activity as MainActivity? mainActivity = activity as MainActivity?
quickLaunchSheet = false quickLaunchSheet = false
infoUtil = InfoUtil(requireContext()) infoUtil = InfoUtil(requireContext())
selectedObjects = arrayListOf()
return fragmentView return fragmentView
} }
@ -136,7 +139,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
//initViews //initViews
shimmerCards = view.findViewById(R.id.shimmer_results_framelayout)
searchBar = view.findViewById(R.id.search_bar) searchBar = view.findViewById(R.id.search_bar)
searchView = view.findViewById(R.id.search_view) searchView = view.findViewById(R.id.search_view)
appBarLayout = view.findViewById(R.id.home_appbarlayout) appBarLayout = view.findViewById(R.id.home_appbarlayout)
@ -160,6 +162,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
recyclerView?.adapter = homeAdapter recyclerView?.adapter = homeAdapter
recyclerView?.enableFastScroll() recyclerView?.enableFastScroll()
shimmerCards = view.findViewById(R.id.shimmer_results_framelayout)
searchSuggestionsAdapter = SearchSuggestionsAdapter( searchSuggestionsAdapter = SearchSuggestionsAdapter(
this, this,
@ -214,8 +219,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} }
if (inputQueries != null) { if (inputQueries != null) {
resultViewModel.deleteAll()
lifecycleScope.launch(Dispatchers.IO){ lifecycleScope.launch(Dispatchers.IO){
resultViewModel.deleteAll()
resultViewModel.parseQueries(inputQueries!!){} resultViewModel.parseQueries(inputQueries!!){}
inputQueries = null inputQueries = null
} }
@ -606,9 +611,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
resultViewModel.addSearchQueryToHistory(q) resultViewModel.addSearchQueryToHistory(q)
} }
} }
resultViewModel.deleteAll()
lifecycleScope.launch(Dispatchers.IO){ lifecycleScope.launch(Dispatchers.IO){
Thread.sleep(300) resultViewModel.deleteAll()
if(sharedPreferences!!.getBoolean("quick_download", false) || sharedPreferences!!.getString("preferred_download_type", "video") == "command"){ 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 (queryList.size == 1 && Patterns.WEB_URL.matcher(queryList.first()).matches()){
if (sharedPreferences!!.getBoolean("download_card", true)) { if (sharedPreferences!!.getBoolean("download_card", true)) {
@ -683,23 +687,28 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} }
override fun onCardClick(videoURL: String, add: Boolean) { override fun onCardClick(videoURL: String, add: Boolean) {
val item = resultsList?.find { it?.url == videoURL } lifecycleScope.launch {
if (add) { val item = resultsList?.find { it?.url == videoURL }
selectedObjects!!.add(item!!) if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
if (actionMode == null){ actionMode?.apply {
actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) if (add) selectedObjects.add(item!!)
}else{ else selectedObjects.remove(item!!)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}"
} if (selectedObjects.size == 0){
} else { this.finish()
selectedObjects!!.remove(item) }else{
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" actionMode?.title = "${selectedObjects.size} ${getString(R.string.selected)}"
if (selectedObjects!!.isEmpty()){ this.menu.findItem(R.id.select_between).isVisible = false
actionMode?.finish() 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) { override fun onCardDetailsClick(videoURL: String) {
@ -739,7 +748,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
private val contextualActionBar = object : ActionMode.Callback { private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.main_menu_context, menu) 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!!.isEnabled = false
searchBar!!.menu.forEach { it.isEnabled = false } searchBar!!.menu.forEach { it.isEnabled = false }
(activity as MainActivity).disableBottomNavigation() (activity as MainActivity).disableBottomNavigation()
@ -758,15 +767,32 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
item: MenuItem? item: MenuItem?
): Boolean { ): Boolean {
return when (item!!.itemId) { 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 -> { R.id.delete_results -> {
val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!) val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!)
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
if (selectedObjects?.size == resultsList?.size){ if (selectedObjects.size == resultsList?.size){
resultViewModel.deleteAll() lifecycleScope.launch(Dispatchers.IO){
resultViewModel.deleteAll()
}
}else{ }else{
resultViewModel.deleteSelected(selectedObjects!!.toList()) resultViewModel.deleteSelected(selectedObjects.toList())
} }
clearCheckedItems() clearCheckedItems()
actionMode?.finish() actionMode?.finish()
@ -776,14 +802,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} }
R.id.download -> { R.id.download -> {
lifecycleScope.launch { lifecycleScope.launch {
if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects!!.size == 1) { if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects.size == 1) {
showSingleDownloadSheet( showSingleDownloadSheet(
selectedObjects!![0], selectedObjects[0],
downloadViewModel.getDownloadType(url = selectedObjects!![0].url) downloadViewModel.getDownloadType(url = selectedObjects[0].url)
) )
}else{ }else{
val downloadList = withContext(Dispatchers.IO){ val downloadList = withContext(Dispatchers.IO){
downloadViewModel.turnResultItemsToDownloadItems(selectedObjects!!) downloadViewModel.turnResultItemsToDownloadItems(selectedObjects)
} }
if (sharedPreferences!!.getBoolean("download_card", true)) { if (sharedPreferences!!.getBoolean("download_card", true)) {
@ -803,8 +829,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} }
R.id.select_all -> { R.id.select_all -> {
homeAdapter?.checkAll(resultsList) homeAdapter?.checkAll(resultsList)
selectedObjects?.clear() selectedObjects.clear()
resultsList?.forEach { selectedObjects?.add(it!!) } resultsList?.forEach { selectedObjects.add(it!!) }
mode?.title = getString(R.string.all_items_selected) mode?.title = getString(R.string.all_items_selected)
true true
} }
@ -812,11 +838,11 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
homeAdapter?.invertSelected(resultsList) homeAdapter?.invertSelected(resultsList)
val invertedList = arrayListOf<ResultItem>() val invertedList = arrayListOf<ResultItem>()
resultsList?.forEach { resultsList?.forEach {
if (!selectedObjects?.contains(it)!!) invertedList.add(it!!) if (!selectedObjects.contains(it)) invertedList.add(it!!)
} }
selectedObjects?.clear() selectedObjects.clear()
selectedObjects?.addAll(invertedList) selectedObjects.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" actionMode!!.title = "${selectedObjects.size} ${getString(R.string.selected)}"
if (invertedList.isEmpty()) actionMode?.finish() if (invertedList.isEmpty()) actionMode?.finish()
true true
} }
@ -836,10 +862,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
private fun clearCheckedItems(){ private fun clearCheckedItems(){
homeAdapter?.clearCheckedItems() homeAdapter?.clearCheckedItems()
selectedObjects?.forEach { selectedObjects.forEach {
homeAdapter?.notifyItemChanged(resultsList!!.indexOf(it)) homeAdapter?.notifyItemChanged(resultsList!!.indexOf(it))
} }
selectedObjects?.clear() selectedObjects.clear()
} }

View file

@ -1,38 +1,46 @@
package com.deniscerri.ytdlnis.ui.adapter package com.deniscerri.ytdlnis.ui.adapter
import android.app.Activity import android.app.Activity
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.util.Extensions.loadThumbnail
import com.deniscerri.ytdlnis.util.Extensions.popup import com.deniscerri.ytdlnis.util.Extensions.popup
import kotlinx.coroutines.CoroutineScope import com.deniscerri.ytdlnis.util.FileUtil
import kotlinx.coroutines.Dispatchers import com.google.android.material.card.MaterialCardView
import kotlinx.coroutines.launch
class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<Pair<DownloadItem, Long?>, AlreadyExistsAdapter.ViewHolder>(AsyncDifferConfig.Builder( class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<Pair<DownloadItem, Long?>, AlreadyExistsAdapter.ViewHolder>(AsyncDifferConfig.Builder(
DIFF_CALLBACK DIFF_CALLBACK
).build()) { ).build()) {
private val onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener
private val activity: Activity private val activity: Activity
private val sharedPreferences: SharedPreferences
init { init {
this.onItemClickListener = onItemClickListener this.onItemClickListener = onItemClickListener
this.activity = activity this.activity = activity
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
} }
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val cardView: ConstraintLayout val cardView: MaterialCardView
init { 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) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) ?: return val alreadyExistsItem = getItem(position) ?: return
val alreadyExistsItem = holder.cardView val item = alreadyExistsItem.first
alreadyExistsItem.tag = item.first.id.toString() val historyID = alreadyExistsItem.second
alreadyExistsItem.popup()
val editBtn = alreadyExistsItem.findViewById<Button>(R.id.already_exists_edit) val card = holder.cardView
val deleteBtn = alreadyExistsItem.findViewById<Button>(R.id.already_exists_delete) card.tag = item.id.toString()
card.popup()
val ttle = alreadyExistsItem.findViewById<Button>(R.id.already_exists_title) val uiHandler = Handler(Looper.getMainLooper())
ttle.text = item.first.title 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 { val fileSize = card.findViewById<TextView>(R.id.file_size)
onItemClickListener.onDeleteItem(item.first, position, item.second) 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 true
} }
editBtn.setOnClickListener { editBtn.setOnClickListener {
onItemClickListener.onEditItem(item.first, position) onItemClickListener.onEditItem(item, position)
} }
deleteBtn.setOnClickListener { deleteBtn.setOnClickListener {
onItemClickListener.onDeleteItem(item.first, position, item.second) onItemClickListener.onDeleteItem(item, position, historyID)
} }
ttle.setOnClickListener(null) card.setOnClickListener(null)
if (item.second != null){ if (historyID != null){
ttle.setOnClickListener { card.setOnClickListener {
onItemClickListener.onShowHistoryItem(item.second!!) onItemClickListener.onShowHistoryItem(historyID)
} }
} }

View file

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

View file

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

View file

@ -22,6 +22,7 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.util.Extensions.loadThumbnail import com.deniscerri.ytdlnis.util.Extensions.loadThumbnail
import com.deniscerri.ytdlnis.util.Extensions.popup import com.deniscerri.ytdlnis.util.Extensions.popup
import com.google.android.material.card.MaterialCardView
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import java.util.* import java.util.*
@ -42,10 +43,10 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
} }
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) { class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
val cardView: ConstraintLayout val cardView: MaterialCardView
init { 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() notifyDataSetChanged()
} }
@SuppressLint("NotifyDataSetChanged")
fun checkMultipleItems(list: List<Long>){
checkedItems.clear()
checkedItems.addAll(list)
notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun invertSelected(items: List<CommandTemplate?>?){ fun invertSelected(items: List<CommandTemplate?>?){
val invertedList = mutableListOf<Long>() val invertedList = mutableListOf<Long>()

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
@ -69,7 +70,10 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(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) 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 //replace existing chip to enable click events
if (selectedCuts.contains(it.title)){ val idx = selectedCuts.indexOfFirst { c -> c.contains(it.title) }
val idx = selectedCuts.indexOf(it.title) if (idx > -1){
val chipForDeletion = chipGroup.children.firstOrNull { cc -> (cc as Chip).text == it.title } val chipForDeletion = chipGroup.children.firstOrNull { cc -> (cc as Chip).text == it.title }
chipGroup.removeView(chipForDeletion) chipGroup.removeView(chipForDeletion)
createChapterChip(it, idx) createChapterChip(it, idx)
@ -456,7 +456,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
player.seekTo(0) player.seekTo(0)
suggestedChips.children.apply { suggestedChips.children.apply {
this.forEach { 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 } 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 { private fun createChapterChip(chapter: ChapterItem, position: Int?) : Chip {
val chip = layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip val chip = layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
chip.text = 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.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK))
chip.isCheckedIconVisible = false chip.isCheckedIconVisible = false
if (position != null) chipGroup.addView(chip, position) if (position != null) chipGroup.addView(chip, position)
else chipGroup.addView(chip) else chipGroup.addView(chip)
if (! selectedCuts.contains(chapter.title))
selectedCuts.add(chip.text.toString()) if (! selectedCuts.contains(timestamp))
selectedCuts.add(timestamp)
listener.onChangeCut(selectedCuts) listener.onChangeCut(selectedCuts)
if (chapter.start_time == 0L && chapter.end_time == 0L) { if (chapter.start_time == 0L && chapter.end_time == 0L) {

View file

@ -305,8 +305,10 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
scheduleBtn.setOnClickListener{ scheduleBtn.setOnClickListener{
UiUtil.showDatePicker(fragmentManager) { UiUtil.showDatePicker(fragmentManager) {
lifecycleScope.launch { lifecycleScope.launch {
resultViewModel.cancelUpdateItemData() withContext(Dispatchers.IO){
resultViewModel.cancelUpdateFormatsItemData() resultViewModel.cancelUpdateItemData()
resultViewModel.cancelUpdateFormatsItemData()
}
} }
scheduleBtn.isEnabled = false scheduleBtn.isEnabled = false
@ -704,8 +706,8 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
lifecycleScope.launch { lifecycleScope.launch {
resultViewModel.cancelUpdateItemData() resultViewModel.cancelUpdateItemData()
resultViewModel.cancelUpdateFormatsItemData() 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 { val listener = object : OnFormatClickListener {
override fun onFormatClick(item: List<FormatTuple>) { override fun onFormatClick(item: List<FormatTuple>) {
downloadItem.format = item.first().format downloadItem.format = item.first().format
downloadItem.videoPreferences.audioFormatIDs.clear()
item.first().audioFormats?.map { it.format_id }?.let { item.first().audioFormats?.map { it.format_id }?.let {
downloadItem.videoPreferences.audioFormatIDs.clear()
downloadItem.videoPreferences.audioFormatIDs.addAll(it) downloadItem.videoPreferences.audioFormatIDs.addAll(it)
} }
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, 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.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
@ -499,7 +500,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
}else{ }else{
val selectedFormats = mutableListOf<Format>() val selectedFormats = mutableListOf<Format>()
formatCollection.forEach { 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()) { if (selectedFormats.isEmpty()) {
items.forEach { _ -> items.forEach { _ ->

View file

@ -1,15 +1,20 @@
package com.deniscerri.ytdlnis.ui.downloadcard package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint
import android.app.ActionBar.LayoutParams import android.app.ActionBar.LayoutParams
import android.app.Dialog import android.app.Dialog
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.TextView
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -27,50 +32,38 @@ import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.deniscerri.ytdlnis.util.Extensions.setTextAndRecalculateWidth import com.deniscerri.ytdlnis.util.Extensions.setTextAndRecalculateWidth
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar 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.ExtendedFloatingActionButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.absoluteValue
class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickListener { class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.OnItemClickListener {
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var listAdapter : PlaylistAdapter private lateinit var listAdapter : PlaylistAdapter
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
private lateinit var ok: MenuItem private lateinit var ok: MaterialButton
private lateinit var toolbar: MaterialToolbar private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var fromTextInput: TextInputLayout private lateinit var fromTextInput: TextInputLayout
private lateinit var toTextInput: 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 items: List<ResultItem?>
private lateinit var itemURLs: List<String>
private lateinit var type: DownloadViewModel.Type private lateinit var type: DownloadViewModel.Type
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.FullScreenDialogTheme)
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::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){ if (Build.VERSION.SDK_INT >= 33){
arguments?.getParcelableArrayList("results", ResultItem::class.java) arguments?.getParcelableArrayList("results", ResultItem::class.java)
@ -85,11 +78,24 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
} }
} }
type = arguments?.getSerializable("type") as DownloadViewModel.Type 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 = listAdapter =
PlaylistAdapter( PlaylistAdapter(
@ -103,7 +109,8 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
recyclerView.enableFastScroll() recyclerView.enableFastScroll()
listAdapter.submitList(items) 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) fromTextInput = view.findViewById(R.id.from_textinput)
toTextInput = view.findViewById(R.id.to_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 if (endNr > items.size) endNr = items.size - 1
listAdapter.checkRange(startNr, endNr) listAdapter.checkRange(startNr, endNr)
ok.isEnabled = true 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 if (endNr > items.size) endNr = items.size -1
listAdapter.checkRange(startNr, endNr) listAdapter.checkRange(startNr, endNr)
ok.isEnabled = true 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 { checkAll!!.setOnClickListener {
if (listAdapter.getCheckedItems().size != items.size){ if (listAdapter.getCheckedItems().size != items.size){
fromTextInput.editText!!.setTextAndRecalculateWidth("1") fromTextInput.editText!!.setTextAndRecalculateWidth("1")
@ -158,7 +165,7 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
fromTextInput.isEnabled = true fromTextInput.isEnabled = true
toTextInput.isEnabled = true toTextInput.isEnabled = true
ok.isEnabled = true ok.isEnabled = true
toolbar.title = resources.getString(R.string.all_items_selected) count.text = resources.getString(R.string.all_items_selected)
}else{ }else{
reset() reset()
fromTextInput.isEnabled = true 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.isEnabled = false
ok.setOnMenuItemClickListener { ok.setOnClickListener {
ok.isEnabled = false ok.isEnabled = false
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val checkedItems = listAdapter.getCheckedItems() val checkedItems = listAdapter.getCheckedItems()
@ -200,7 +207,7 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
downloadViewModel.insertToProcessing(downloadItems) downloadViewModel.insertToProcessing(downloadItems)
withContext(Dispatchers.Main){ 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 true
} }
view.findViewById<BottomAppBar>(R.id.bottomAppBar).setOnMenuItemClickListener { m: MenuItem -> val bottomAppBar = view.findViewById<BottomAppBar>(R.id.bottomAppBar)
val itemId = m.itemId bottomAppBar.setOnMenuItemClickListener { m: MenuItem ->
if (itemId == R.id.invert_selected) { when(m.itemId) {
listAdapter.invertSelected(items) R.id.invert_selected -> {
val checkedItems = listAdapter.getCheckedItems() listAdapter.invertSelected(items)
if (checkedItems.size == items.size){ val checkedItems = listAdapter.getCheckedItems()
toolbar.title= resources.getString(R.string.all_items_selected) if (checkedItems.size == items.size){
}else{ count.text = resources.getString(R.string.all_items_selected)
toolbar.title = "${checkedItems.size} ${resources.getString(R.string.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){ R.id.select_between -> {
fromTextInput.isEnabled = false val selectedItems = listAdapter.getCheckedItems()
toTextInput.isEnabled = false 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 true
} }
toolbar.setNavigationOnClickListener { selectBetween = bottomAppBar.menu.findItem(R.id.select_between)
dismiss() itemURLs = items.map { it!!.url }
}
} }
private fun checkRanges(start: String, end: String) : Boolean { private fun checkRanges(start: String, end: String) : Boolean {
return start.isNotBlank() && end.isNotBlank() return start.isNotBlank() && end.isNotBlank()
} }
@ -241,15 +263,15 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
private fun reset(){ private fun reset(){
ok.isEnabled = false ok.isEnabled = false
listAdapter.clearCheckeditems() 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>) { override fun onCardSelect(itemURL: String, isChecked: Boolean, checkedItems: List<String>) {
if (checkedItems.size == items.size){ if (checkedItems.size == items.size){
toolbar.title = resources.getString(R.string.all_items_selected) count.text = resources.getString(R.string.all_items_selected)
}else{ }else{
toolbar.title = "${checkedItems.size} ${resources.getString(R.string.selected)}" count.text = "${checkedItems.size} ${resources.getString(R.string.selected)}"
} }
if (checkedItems.isEmpty()){ if (checkedItems.isEmpty()){
ok.isEnabled = false ok.isEnabled = false
@ -259,6 +281,17 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
ok.isEnabled = true ok.isEnabled = true
fromTextInput.isEnabled = false fromTextInput.isEnabled = false
toTextInput.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() this.finish()
}else{ }else{
this.title = "$selectedObjects ${getString(R.string.selected)}" this.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.select_between).isVisible = false
if (selectedObjects == 2){ if (selectedObjects == 2){
val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it } val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){ val idsInMiddle = withContext(Dispatchers.IO){

View file

@ -161,6 +161,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
this.finish() this.finish()
}else{ }else{
actionMode?.title = "$selectedObjects ${getString(R.string.selected)}" actionMode?.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.select_between).isVisible = false
if(selectedObjects == 2){ if(selectedObjects == 2){
val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it } val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){ val idsInMiddle = withContext(Dispatchers.IO){
@ -213,7 +214,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
lifecycleScope.launch { lifecycleScope.launch {
val selectedIDs = getSelectedIDs().sortedBy { it } val selectedIDs = getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){ 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() }.toMutableList()
idsInMiddle.addAll(selectedIDs) idsInMiddle.addAll(selectedIDs)
if (idsInMiddle.isNotEmpty()){ if (idsInMiddle.isNotEmpty()){

View file

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

View file

@ -159,6 +159,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
this.finish() this.finish()
}else{ }else{
actionMode?.title = "$selectedObjects ${getString(R.string.selected)}" actionMode?.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.select_between).isVisible = false
if(selectedObjects == 2){ if(selectedObjects == 2){
val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it } val selectedIDs = contextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){ val idsInMiddle = withContext(Dispatchers.IO){
@ -207,7 +208,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
lifecycleScope.launch { lifecycleScope.launch {
val selectedIDs = getSelectedIDs().sortedBy { it } val selectedIDs = getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){ 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() }.toMutableList()
idsInMiddle.addAll(selectedIDs) idsInMiddle.addAll(selectedIDs)
if (idsInMiddle.isNotEmpty()){ if (idsInMiddle.isNotEmpty()){

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.more package com.deniscerri.ytdlnis.ui.more
import android.annotation.SuppressLint
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
@ -62,7 +63,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
private lateinit var noResults: RelativeLayout private lateinit var noResults: RelativeLayout
private lateinit var mainActivity: MainActivity private lateinit var mainActivity: MainActivity
private lateinit var sortChip: Chip private lateinit var sortChip: Chip
private var selectedObjects: ArrayList<CommandTemplate>? = null private lateinit var selectedObjects: ArrayList<CommandTemplate>
private var actionMode : ActionMode? = null private var actionMode : ActionMode? = null
private val jsonFormat = Json { prettyPrint = true } 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) return inflater.inflate(R.layout.fragment_command_templates, container, false)
} }
@SuppressLint("RestrictedApi")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
templatesList = listOf() templatesList = listOf()
@ -273,21 +275,26 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
} }
override fun onCardSelect(itemID: Long, isChecked: Boolean) { override fun onCardSelect(itemID: Long, isChecked: Boolean) {
val item = templatesList.find { it.id == itemID } lifecycleScope.launch {
if (isChecked) { val item = templatesList.find { it.id == itemID }
selectedObjects!!.add(item!!) if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
if (actionMode == null){ actionMode?.apply {
actionMode = (activity as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) if (isChecked) selectedObjects.add(item!!)
else selectedObjects.remove(item!!)
}else{ if (selectedObjects.size == 0){
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" this.finish()
} }else{
} actionMode?.title = "${selectedObjects.size} ${getString(R.string.selected)}"
else { this.menu.findItem(R.id.select_between).isVisible = false
selectedObjects!!.remove(item) if(selectedObjects.size == 2){
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" val selectedIDs = selectedObjects.sortedBy { it.id }
if (selectedObjects!!.isEmpty()){ val resultsInMiddle = withContext(Dispatchers.IO){
actionMode?.finish() 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 { private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.templates_menu_context, menu) 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 return true
} }
@ -311,12 +318,27 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
item: MenuItem? item: MenuItem?
): Boolean { ): Boolean {
return when (item!!.itemId) { 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 -> { R.id.delete_results -> {
val deleteDialog = MaterialAlertDialogBuilder(requireContext()) val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){ for (obj in selectedObjects){
commandTemplateViewModel.delete(obj) commandTemplateViewModel.delete(obj)
} }
clearCheckedItems() clearCheckedItems()
@ -327,8 +349,8 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
} }
R.id.select_all -> { R.id.select_all -> {
templatesAdapter.checkAll(templatesList) templatesAdapter.checkAll(templatesList)
selectedObjects?.clear() selectedObjects.clear()
templatesList.forEach { selectedObjects?.add(it) } templatesList.forEach { selectedObjects.add(it) }
mode?.title = getString(R.string.all_items_selected) mode?.title = getString(R.string.all_items_selected)
true true
} }
@ -336,11 +358,11 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
templatesAdapter.invertSelected(templatesList) templatesAdapter.invertSelected(templatesList)
val invertedList = arrayListOf<CommandTemplate>() val invertedList = arrayListOf<CommandTemplate>()
templatesList.forEach { templatesList.forEach {
if (!selectedObjects?.contains(it)!!) invertedList.add(it) if (!selectedObjects.contains(it)!!) invertedList.add(it)
} }
selectedObjects?.clear() selectedObjects.clear()
selectedObjects?.addAll(invertedList) selectedObjects.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" actionMode!!.title = "${selectedObjects.size} ${getString(R.string.selected)}"
if (invertedList.isEmpty()) actionMode?.finish() if (invertedList.isEmpty()) actionMode?.finish()
true true
} }
@ -348,7 +370,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
lifecycleScope.launch{ lifecycleScope.launch{
val output = jsonFormat.encodeToString( val output = jsonFormat.encodeToString(
CommandTemplateExport( CommandTemplateExport(
templates = selectedObjects!!.toList(), templates = selectedObjects.toList(),
shortcuts = listOf() shortcuts = listOf()
) )
) )
@ -374,7 +396,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
private fun clearCheckedItems(){ private fun clearCheckedItems(){
templatesAdapter.clearCheckeditems() templatesAdapter.clearCheckeditems()
selectedObjects?.clear() selectedObjects.clear()
} }
private var simpleCallback: ItemTouchHelper.SimpleCallback = 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.models.CookieItem
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.ui.adapter.CookieAdapter import com.deniscerri.ytdlnis.ui.adapter.CookieAdapter
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
@ -128,7 +129,9 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
cookiesViewModel.deleteAll() cookiesViewModel.deleteAll()
kotlin.runCatching { kotlin.runCatching {
File(context?.cacheDir, "cookies.txt").apply { writeText("") } FileUtil.getCookieFile(requireContext(), true){
File(it).apply { writeText("") }
}
} }
} }
deleteDialog.show() 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.models.CookieItem
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.ui.BaseActivity import com.deniscerri.ytdlnis.ui.BaseActivity
import com.deniscerri.ytdlnis.util.Extensions.convertNetscapeToSetCookie
import com.google.accompanist.web.AccompanistWebChromeClient import com.google.accompanist.web.AccompanistWebChromeClient
import com.google.accompanist.web.AccompanistWebViewClient import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.WebView import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState 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.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@ -49,16 +49,15 @@ class WebViewActivity : BaseActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.webview_activity) setContentView(R.layout.webview_activity)
url = intent.extras!!.getString("url")!! url = intent.extras!!.getString("url")!!
cookies = ""
cookiesViewModel = ViewModelProvider(this)[CookieViewModel::class.java] cookiesViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
val appbar = findViewById<AppBarLayout>(R.id.webview_appbarlayout) lifecycleScope.launch {
toolbar = appbar.findViewById(R.id.webviewToolbar) val appbar = findViewById<AppBarLayout>(R.id.webview_appbarlayout)
generateBtn = toolbar.findViewById(R.id.generate) toolbar = appbar.findViewById(R.id.webviewToolbar)
webViewCompose = findViewById(R.id.webview_compose) generateBtn = toolbar.findViewById(R.id.generate)
cookieManager = CookieManager.getInstance() webViewCompose = findViewById(R.id.webview_compose)
preferences = PreferenceManager.getDefaultSharedPreferences(this) cookieManager = CookieManager.getInstance()
preferences = PreferenceManager.getDefaultSharedPreferences(this@WebViewActivity)
lifecycleScope.launch{
webViewClient = object : AccompanistWebViewClient() { webViewClient = object : AccompanistWebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url) super.onPageFinished(view, url)
@ -78,9 +77,7 @@ class WebViewActivity : BaseActivity() {
generateBtn.setOnClickListener { generateBtn.setOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO) {
val a = cookieManager.getCookie(url).replace(";", "\n")
Log.e("TESTING", a)
cookiesViewModel.getCookiesFromDB(url).getOrNull()?.let { cookiesViewModel.getCookiesFromDB(url).getOrNull()?.let {
kotlin.runCatching { kotlin.runCatching {
cookiesViewModel.insert( cookiesViewModel.insert(
@ -92,12 +89,17 @@ class WebViewActivity : BaseActivity() {
) )
cookiesViewModel.updateCookiesFile() cookiesViewModel.updateCookiesFile()
}.onFailure { }.onFailure {
withContext(Dispatchers.Main){ withContext(Dispatchers.Main) {
Toast.makeText(this@WebViewActivity, "Something went wrong", Toast.LENGTH_SHORT).show() Toast.makeText(
this@WebViewActivity,
"Something went wrong",
Toast.LENGTH_SHORT
).show()
} }
} }
} }
withContext(Dispatchers.Main){ cookieManager.removeAllCookies(null)
withContext(Dispatchers.Main) {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
} }
@ -109,7 +111,6 @@ class WebViewActivity : BaseActivity() {
} }
} }
} }
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")

View file

@ -45,7 +45,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
private lateinit var topAppBar: MaterialToolbar private lateinit var topAppBar: MaterialToolbar
private lateinit var mainActivity: MainActivity private lateinit var mainActivity: MainActivity
private lateinit var logViewModel: LogViewModel private lateinit var logViewModel: LogViewModel
private var selectedObjects: ArrayList<LogItem>? = null private lateinit var selectedObjects: ArrayList<LogItem>
private var actionMode : ActionMode? = null private var actionMode : ActionMode? = null
override fun onCreateView( override fun onCreateView(
@ -139,18 +139,18 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
override fun onCardSelect(itemID: Long, isChecked: Boolean) { override fun onCardSelect(itemID: Long, isChecked: Boolean) {
val item = items.find { it.id == itemID } val item = items.find { it.id == itemID }
if (isChecked) { if (isChecked) {
selectedObjects!!.add(item!!) selectedObjects.add(item!!)
if (actionMode == null){ if (actionMode == null){
actionMode = (activity as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar) actionMode = (activity as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
}else{ }else{
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" actionMode!!.title = "${selectedObjects.size} ${getString(R.string.selected)}"
} }
} }
else { else {
selectedObjects!!.remove(item) selectedObjects.remove(item)
actionMode?.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" actionMode?.title = "${selectedObjects.size} ${getString(R.string.selected)}"
if (selectedObjects!!.isEmpty()){ if (selectedObjects.isEmpty()){
actionMode?.finish() actionMode?.finish()
} }
} }
@ -159,7 +159,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
private val contextualActionBar = object : ActionMode.Callback { private val contextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.logs_menu_context, menu) 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 return true
} }
@ -180,7 +180,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
for (obj in selectedObjects!!){ for (obj in selectedObjects){
logViewModel.delete(obj) logViewModel.delete(obj)
} }
clearCheckedItems() clearCheckedItems()
@ -191,8 +191,8 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
} }
R.id.select_all -> { R.id.select_all -> {
downloadLogAdapter.checkAll(items) downloadLogAdapter.checkAll(items)
selectedObjects?.clear() selectedObjects.clear()
items.forEach { selectedObjects?.add(it) } items.forEach { selectedObjects.add(it) }
mode?.title = getString(R.string.all_items_selected) mode?.title = getString(R.string.all_items_selected)
true true
} }
@ -200,11 +200,11 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
downloadLogAdapter.invertSelected(items) downloadLogAdapter.invertSelected(items)
val invertedList = arrayListOf<LogItem>() val invertedList = arrayListOf<LogItem>()
items.forEach { items.forEach {
if (!selectedObjects?.contains(it)!!) invertedList.add(it) if (!selectedObjects.contains(it)!!) invertedList.add(it)
} }
selectedObjects?.clear() selectedObjects.clear()
selectedObjects?.addAll(invertedList) selectedObjects.addAll(invertedList)
actionMode!!.title = "${selectedObjects!!.size} ${getString(R.string.selected)}" actionMode!!.title = "${selectedObjects.size} ${getString(R.string.selected)}"
if (invertedList.isEmpty()) actionMode?.finish() if (invertedList.isEmpty()) actionMode?.finish()
true true
} }
@ -220,7 +220,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
private fun clearCheckedItems(){ private fun clearCheckedItems(){
downloadLogAdapter.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.activity.result.contract.ActivityResultContracts
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo import androidx.work.WorkInfo
@ -30,6 +31,8 @@ class FolderSettingsFragment : BaseSettingsFragment() {
private var videoPath: Preference? = null private var videoPath: Preference? = null
private var commandPath: Preference? = null private var commandPath: Preference? = null
private var accessAllFiles : Preference? = null private var accessAllFiles : Preference? = null
private var noFragments: SwitchPreferenceCompat? = null
private var keepFragments: SwitchPreferenceCompat? = null
private var cacheDownloads : Preference? = null private var cacheDownloads : Preference? = null
private var audioFilenameTemplate : Preference? = null private var audioFilenameTemplate : Preference? = null
private var videoFilenameTemplate : Preference? = null private var videoFilenameTemplate : Preference? = null
@ -50,6 +53,8 @@ class FolderSettingsFragment : BaseSettingsFragment() {
videoPath = findPreference("video_path") videoPath = findPreference("video_path")
commandPath = findPreference("command_path") commandPath = findPreference("command_path")
accessAllFiles = findPreference("access_all_files") accessAllFiles = findPreference("access_all_files")
noFragments = findPreference("no_part")
keepFragments = findPreference("keep_cache")
cacheDownloads = findPreference("cache_downloads") cacheDownloads = findPreference("cache_downloads")
videoFilenameTemplate = findPreference("file_name_template") videoFilenameTemplate = findPreference("file_name_template")
audioFilenameTemplate = findPreference("file_name_template_audio") audioFilenameTemplate = findPreference("file_name_template_audio")
@ -116,6 +121,17 @@ class FolderSettingsFragment : BaseSettingsFragment() {
true 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?.title = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
videoFilenameTemplate?.summary = preferences.getString("file_name_template", "%(uploader)s - %(title)s") videoFilenameTemplate?.summary = preferences.getString("file_name_template", "%(uploader)s - %(title)s")
audioFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]" 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() == null) return@observe
if (list.first().state == WorkInfo.State.SUCCEEDED){ 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)}" 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.lifecycleScope
import androidx.lifecycle.withStarted import androidx.lifecycle.withStarted
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -40,6 +41,7 @@ import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder import me.zhanghai.android.fastscroll.FastScrollerBuilder
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.net.HttpCookie
import java.util.Locale import java.util.Locale
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.abs import kotlin.math.abs
@ -225,22 +227,26 @@ object Extensions {
number = nr number = nr
verticalOffset = 3 verticalOffset = 3
horizontalOffset = horizontalOffset =
if (nr < 10) 7 if (nr < 10) dp(App.instance.resources, 7f)
else if (nr < 100) 10 else if (nr < 100) dp(App.instance.resources, 10f)
else 20 else dp(App.instance.resources, 20f)
} }
} }
} }
fun String.appendLineToLog(line: String): String { fun String.appendLineToLog(line: String): String {
val lines = this.split("\n") val lines = this.split("\n")
//clean dublicate progress + add newline if (!lines.takeLast(3).contains(line)){
var newLine = line //clean dublicate progress + add newline
if (newLine.contains("[download")) { var newLine = line
newLine = "[download]" + line.split("[download]").last() 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){ 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 { 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) { fun deleteConfigFiles(request: YoutubeDLRequest) {
request.getArguments("--config")?.forEach { runCatching {
if (it != null) File(it).delete() request.getArguments("--config")?.forEach {
} if (it != null) File(it).delete()
request.getArguments("--config-locations")?.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 { fun getDownloadArchivePath(context: Context) : String {
return context.cacheDir.absolutePath + "/download_archive.txt" return context.filesDir.absolutePath + "/download_archive.txt"
} }
fun getDefaultTerminalPath() : String { fun getDefaultTerminalPath() : String {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/TERMINAL_CACHE" 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") 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{ fun convertFileSize(s: Long): String{

View file

@ -249,8 +249,8 @@ class InfoUtil(private val context: Context) {
formats.groupBy { it.format_id }.forEach { formats.groupBy { it.format_id }.forEach {
if (it.value.count() > 1) { 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}" } 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) } val defaultLang = it.value.find { f -> f.format_note.contains("original", true) }
engDefault?.format_id = (engDefault?.format_id?.split("-")?.get(0) ?: "") + "-${it.value.size-1}" defaultLang?.format_id = (defaultLang?.format_id?.split("-")?.get(0) ?: "") + "-${it.value.size-1}"
} }
} }
formats.sortByDescending { it.filesize } formats.sortByDescending { it.filesize }
@ -648,9 +648,9 @@ class InfoUtil(private val context: Context) {
if (result.isNullOrBlank()) continue if (result.isNullOrBlank()) continue
val jsonObject = JSONObject(result) val jsonObject = JSONObject(result)
val title = jsonObject.getStringByAny("track", "alt_title", "title", "webpage_url_basename") 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() var duration = jsonObject.getIntByAny("duration").toString()
if (duration != "-1"){ if (duration != "-1"){
duration = jsonObject.getInt("duration").toStringDuration(Locale.US) duration = jsonObject.getInt("duration").toStringDuration(Locale.US)
@ -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") val website = jsonObject.getStringByAny("ie_key", "extractor_key", "extractor")
var playlistTitle = jsonObject.getStringByAny("playlist_title") var playlistTitle = jsonObject.getStringByAny("playlist_title")
var playlistURL: String? = "" var playlistURL: String? = ""
@ -991,11 +1002,15 @@ class InfoUtil(private val context: Context) {
val keepCache = sharedPreferences.getBoolean("keep_cache", false) val keepCache = sharedPreferences.getBoolean("keep_cache", false)
if(keepCache){ if(keepCache){
request.addOption("--part")
request.addOption("--keep-fragments") request.addOption("--keep-fragments")
} }
if (sharedPreferences.getBoolean("no_part", false)){
request.addOption("--no-part")
}
val embedMetadata = sharedPreferences.getBoolean("embed_metadata", true) val embedMetadata = sharedPreferences.getBoolean("embed_metadata", true)
val thumbnailFormat = sharedPreferences.getString("thumbnail_format", "jpg")
var filenameTemplate = downloadItem.customFileNameTemplate var filenameTemplate = downloadItem.customFileNameTemplate
if(downloadItem.type != DownloadViewModel.Type.command){ if(downloadItem.type != DownloadViewModel.Type.command){
@ -1003,7 +1018,7 @@ class InfoUtil(private val context: Context) {
if (downloadItem.SaveThumb) { if (downloadItem.SaveThumb) {
request.addOption("--write-thumbnail") request.addOption("--write-thumbnail")
request.addOption("--convert-thumbnails", sharedPreferences.getString("thumbnail_format", "jpg")!!) request.addOption("--convert-thumbnails", thumbnailFormat!!)
} }
if (!sharedPreferences.getBoolean("mtime", false)){ if (!sharedPreferences.getBoolean("mtime", false)){
request.addOption("--no-mtime") 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()){ 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()){ 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)$") request.addOption("--parse-metadata", "uploader:(?P<uploader>.+)(?: - Topic)$")
@ -1059,10 +1065,7 @@ class InfoUtil(private val context: Context) {
if (downloadItem.downloadSections.isNotBlank()){ if (downloadItem.downloadSections.isNotBlank()){
downloadItem.downloadSections.split(";").forEach { downloadItem.downloadSections.split(";").forEach {
if (it.isBlank()) return@forEach if (it.isBlank()) return@forEach
if (it.contains(":")) request.addOption("--download-sections", "*${it.split(" ")[0]}")
request.addOption("--download-sections", "*$it")
else
request.addOption("--download-sections", it)
if (sharedPreferences.getBoolean("force_keyframes", false) && !request.hasOption("--force-keyframes-at-cuts")){ if (sharedPreferences.getBoolean("force_keyframes", false) && !request.hasOption("--force-keyframes-at-cuts")){
request.addOption("--force-keyframes-at-cuts") request.addOption("--force-keyframes-at-cuts")
@ -1121,6 +1124,13 @@ class InfoUtil(private val context: Context) {
val ext = downloadItem.container val ext = downloadItem.container
if (audioQualityId.isNotBlank()) { 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"){ if (audioQualityId.contains("-") && !downloadItem.format.lang.isNullOrBlank() && downloadItem.format.lang != "None"){
audioQualityId = "ba[format_id~='^(${audioQualityId.split("-")[0]})'][language^=${downloadItem.format.lang}]/ba/b" 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()) { if (downloadItem.playlistTitle.isNotEmpty()) {
request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s") 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 { } else {
request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s") 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") request.addOption("--embed-thumbnail")
if (! request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", "jpg") if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!)
val cropThumb = downloadItem.audioPreferences.cropThumb ?: sharedPreferences.getBoolean("crop_thumbnail", true)
if (cropThumb){ if (cropThumb){
try { runCatching {
val config = File(context.cacheDir.absolutePath + "/config" + downloadItem.id + "##ffmpegCrop.txt") 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) config.writeText(configData)
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
request.addOption("--config", config.absolutePath) 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") val acodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac")
audioFormats.forEachIndexed { idx, it -> formats.add(Format(audioFormatsValues[idx], containerPreference!!,"",acodecPreference!!, "",0, it)) } audioFormats.forEachIndexed { idx, it -> formats.add(Format(audioFormatsValues[idx], containerPreference!!,"",acodecPreference!!, "",0, it)) }
audioFormats.reverse() 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 return formats
} }
@ -1449,10 +1459,24 @@ class InfoUtil(private val context: Context) {
val videoFormats = resources.getStringArray(R.array.video_formats_values) val videoFormats = resources.getStringArray(R.array.video_formats_values)
val formats = mutableListOf<Format>() val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("video_format", "") val containerPreference = sharedPreferences.getString("video_format", "")
val acodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac") val audioCodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac")!!.run {
val vcodecPreference = sharedPreferences.getString("video_codec", "avc|h264")!!.ifEmpty { resources.getString(R.string.defaultValue) } if (this.isNotEmpty()){
videoFormats.reversed().forEach { formats.add(Format(it, containerPreference!!,vcodecPreference,acodecPreference!!, "",0, it.split("_")[0])) } val audioCodecs = resources.getStringArray(R.array.audio_codec)
formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,vcodecPreference,acodecPreference!!, "",1, it)) } 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 return formats
} }

View file

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

View file

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

View file

@ -185,7 +185,7 @@ class UpdateUtil(var context: Context) {
.setTitle(activity.getString(R.string.changelog)) .setTitle(activity.getString(R.string.changelog))
.setView(scrollView) .setView(scrollView)
.setIcon(R.drawable.ic_chapters) .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 { Handler(Looper.getMainLooper()).post {
changeLogDialog.show() changeLogDialog.show()
} }

View file

@ -88,7 +88,8 @@ class DownloadWorker(
queuedItems.collect { items -> queuedItems.collect { items ->
runningYTDLInstances.clear() runningYTDLInstances.clear()
dao.getActiveDownloadsList().forEach { val activeDownloads = dao.getActiveDownloadsList()
activeDownloads.forEach {
runningYTDLInstances.add(it.id) runningYTDLInstances.add(it.id)
} }
@ -155,7 +156,7 @@ class DownloadWorker(
runCatching { runCatching {
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> 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 } val title: String = downloadItem.title.ifEmpty { downloadItem.url }
notificationUtil.updateDownloadNotification( notificationUtil.updateDownloadNotification(
downloadItem.id.toInt(), downloadItem.id.toInt(),
@ -267,7 +268,7 @@ class DownloadWorker(
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
notificationUtil.createDownloadFinished( 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){ if (wasQuickDownloaded && createResultItem){
@ -335,7 +336,7 @@ class DownloadWorker(
setProgressAsync(workDataOf("progress" to 100, "output" to it.toString(), "id" to downloadItem.id)) setProgressAsync(workDataOf("progress" to 100, "output" to it.toString(), "id" to downloadItem.id))
} }
if (items.size <= 1) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) //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.App
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
@ -28,7 +29,8 @@ class MoveCacheFilesWorker(
val notificationUtil = NotificationUtil(App.instance) val notificationUtil = NotificationUtil(App.instance)
val id = System.currentTimeMillis().toInt() 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() val allContent = downloadFolders.walk()
allContent.drop(1) allContent.drop(1)
val totalFiles = allContent.count() val totalFiles = allContent.count()
@ -44,7 +46,7 @@ class MoveCacheFilesWorker(
allContent.forEach { allContent.forEach {
progress++ progress++
notificationUtil.updateCacheMovingNotification(id, progress, totalFiles) 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) { if (it.isDirectory) {
destFile.mkdirs() destFile.mkdirs()
return@forEach return@forEach

View file

@ -64,9 +64,8 @@ class TerminalDownloadWorker(
) )
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
val cookiesFile = File(context.cacheDir, "cookies.txt") FileUtil.getCookieFile(context){
if (cookiesFile.exists()){ request.addOption("--cookies", it)
request.addOption("--cookies", cookiesFile.absolutePath)
} }
val useHeader = sharedPreferences.getBoolean("use_header", false) val useHeader = sharedPreferences.getBoolean("use_header", false)
@ -144,7 +143,6 @@ class TerminalDownloadWorker(
return runBlocking { return runBlocking {
if (logDownloads) logRepo.update(it.out, logItem.id) if (logDownloads) logRepo.update(it.out, logItem.id)
dao.updateLog(it.out, itemId.toLong()) dao.updateLog(it.out, itemId.toLong())
Thread.sleep(1000)
dao.delete(itemId.toLong()) dao.delete(itemId.toLong())
notificationUtil.cancelDownloadNotification(itemId) notificationUtil.cancelDownloadNotification(itemId)
@ -156,7 +154,6 @@ class TerminalDownloadWorker(
if (it.message != null){ if (it.message != null){
if (logDownloads) logRepo.update(it.message!!, logItem.id) if (logDownloads) logRepo.update(it.message!!, logItem.id)
dao.updateLog(it.message!!, itemId.toLong()) dao.updateLog(it.message!!, itemId.toLong())
Thread.sleep(1000)
dao.delete(itemId.toLong()) 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"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView android:id="@+id/download_card_view"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/already_exists_constraint"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_marginBottom="10dp" android:layout_height="wrap_content"
android:backgroundTint="@android:color/transparent"
android:checkable="true" android:checkable="true"
android:paddingHorizontal="20dp"
android:paddingBottom="5dp"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:background="?android:attr/selectableItemBackground" app:checkedIcon="@null"
android:layout_height="wrap_content"> 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 <TextView
android:id="@+id/already_exists_title" android:id="@+id/duration"
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"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:icon="@drawable/ic_edit" android:layout_margin="5dp"
android:contentDescription="@string/edit" android:background="#80000000"
app:layout_constraintBottom_toBottomOf="@id/already_exists_title" android:clickable="false"
app:layout_constraintEnd_toEndOf="parent" android:ellipsize="end"
app:layout_constraintTop_toTopOf="@id/already_exists_title" android:gravity="center"
app:layout_constraintVertical_bias="1.0" /> 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" />
<androidx.constraintlayout.widget.ConstraintLayout
<Button android:id="@+id/download_item_data"
android:id="@+id/already_exists_delete" android:layout_width="0dp"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:contentDescription="@string/Remove"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:icon="@drawable/baseline_delete_24" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toBottomOf="@id/already_exists_title"
app:layout_constraintEnd_toEndOf="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> </androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout <LinearLayout
android:id="@+id/multiple_top_info"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">

View file

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

View file

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

View file

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

View file

@ -1,121 +1,105 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView android:id="@+id/playlist_card"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/playlist_card_constraintLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_marginBottom="10dp" android:layout_height="wrap_content"
android:checkable="true" android:checkable="true"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:background="?android:attr/selectableItemBackground" app:strokeWidth="0dp"
android:layout_height="wrap_content"> 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" <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp" android:layout_width="match_parent"
android:paddingHorizontal="10dp"
android:paddingVertical="3dp"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:cardPreventCornerOverlap="true" android:paddingHorizontal="20dp"
app:layout_constraintBottom_toBottomOf="parent" android:paddingVertical="10dp">
app:layout_constraintEnd_toStartOf="@+id/checkBox"
app:layout_constraintStart_toStartOf="parent" <com.google.android.material.imageview.ShapeableImageView
app:layout_constraintTop_toTopOf="parent" android:id="@+id/downloads_image_view"
app:strokeWidth="0dp"> 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 <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:id="@+id/download_item_data"
android:layout_height="match_parent"> android:layout_width="0dp"
android:layout_height="wrap_content"
<com.google.android.material.imageview.ShapeableImageView app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/downloads_image_view" app:layout_constraintEnd_toStartOf="@+id/checkBox"
android:clickable="false" app:layout_constraintHorizontal_weight="0.7"
android:layout_width="0dp" app:layout_constraintStart_toEndOf="@+id/downloads_image_view"
android:layout_height="0dp" app:layout_constraintTop_toTopOf="parent">
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"
/>
<TextView <TextView
android:id="@+id/duration" android:id="@+id/title"
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:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
app:layout_constraintHorizontal_weight="0.7" android:ellipsize="end"
app:layout_constraintBottom_toBottomOf="parent" android:maxLines="2"
android:paddingHorizontal="10dp"
android:scrollbars="none"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/downloads_image_view" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> 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>
<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> </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"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
@ -19,16 +18,16 @@
android:id="@+id/bottomAppBar" android:id="@+id/bottomAppBar"
style="@style/Widget.Material3.BottomAppBar" style="@style/Widget.Material3.BottomAppBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="70dp"
app:menu="@menu/select_playlist_menu" app:menu="@menu/select_playlist_menu"
android:layout_gravity="bottom"> android:layout_gravity="bottom">
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/from_textinput" android:id="@+id/from_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox" style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="40dp" android:layout_marginStart="50dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:hint="@string/start"> android:hint="@string/start">
@ -41,7 +40,7 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/to_textinput" android:id="@+id/to_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox" style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/end" android:hint="@string/end"
@ -58,12 +57,20 @@
</com.google.android.material.bottomappbar.BottomAppBar> </com.google.android.material.bottomappbar.BottomAppBar>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/check_all" android:id="@+id/check_all"
android:layout_width="wrap_content" style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Primary"
app:icon="@drawable/ic_select_all" android:layout_width="55dp"
android:layout_height="wrap_content" app:elevation="0dp"
app:layout_anchor="@id/bottomAppBar"/> 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> </androidx.coordinatorlayout.widget.CoordinatorLayout>
@ -79,44 +86,45 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:paddingTop="20dp">
<TextView
<com.google.android.material.appbar.AppBarLayout android:id="@+id/bottom_sheet_title"
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
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:maxLines="2"
android:layout_marginHorizontal="20dp" 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_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@+id/appbar"
app:layout_constraintVertical_bias="0.0">
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
@ -124,9 +132,8 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloadMultipleRecyclerview" android:id="@+id/downloadMultipleRecyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
tools:listitem="@layout/playlist_item" android:paddingTop="10dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:focusableInTouchMode="true"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="80dp" android:paddingBottom="80dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

View file

@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity" > 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 <item
android:id="@+id/delete_results" android:id="@+id/delete_results"
android:title="@string/remove_results" android:title="@string/remove_results"

View file

@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity" > 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 <item
android:id="@+id/delete_results" android:id="@+id/delete_results"
android:title="@string/remove_results" android:title="@string/remove_results"

View file

@ -3,6 +3,13 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:theme="@style/BaseTheme"> 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" <item android:title="@string/invert_selected"
android:icon="@drawable/ic_terminal" android:icon="@drawable/ic_terminal"
app:showAsAction="never" app:showAsAction="never"

View file

@ -3,6 +3,13 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity" > 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 <item
android:id="@+id/delete_results" android:id="@+id/delete_results"
android:title="@string/remove_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="all_queries_finished">All Queries have been processed</string>
<string name="select_between">Select Items Between</string> <string name="select_between">Select Items Between</string>
<string name="reset_processed_links">Reset Already Processed Links</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> </resources>

View file

@ -36,6 +36,15 @@
app:title="@string/cache_first" /> 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 <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false" app:defaultValue="false"

View file

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

View file

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

View file

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