other stuff

- formats auto updating as soon as the download card opens if auto-update is on
- added preferred audio format always in the video tab
- made app post downloads for queue in chunks
- made app always save logs in case it fails, and if succeeds and logs are off it deletes it
- fixed app navigating to home screen when cancelling download card in history screen
- added a button to skip incoming app update so it wont bother you anymore
- fixed settings not restoring some fields
- fixed crunchyroll not working with cookies
- added search for command templates
- added sort filtering for command templates
- added all shortcuts inside filename template creation dialog. Long click them to see the explanation
- added preference to hide elements from the download card
- made avc1 and m4a as preferred codecs for noobs
- create dialog when download item exists. U can edit the download item right from there or access the history item to view the file
This commit is contained in:
deniscerri 2023-10-22 14:51:23 +02:00
parent b636bf199c
commit 9353e08ee9
No known key found for this signature in database
GPG key ID: 95C43D517D830350
60 changed files with 1998 additions and 1100 deletions

View file

@ -10,7 +10,7 @@ def properties = new Properties()
def versionMajor = 1
def versionMinor = 6
def versionPatch = 8
def versionBuild = 2 // bump for dogfood builds, public betas, etc.
def versionBuild = 3 // bump for dogfood builds, public betas, etc.
def versionExt = ""
if (versionBuild > 0){
@ -134,7 +134,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation "androidx.navigation:navigation-fragment-ktx:$navVer"
implementation "androidx.navigation:navigation-ui-ktx:$navVer"
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.test.ext:junit-ktx:1.1.5'
implementation 'androidx.compose.ui:ui-android:1.5.3'
testImplementation "junit:junit:$junitVer"
@ -157,10 +157,10 @@ dependencies {
implementation "androidx.room:room-runtime:2.5.2"
implementation "androidx.room:room-ktx:2.5.2"
ksp "androidx.room:room-compiler:2.5.2"
implementation 'androidx.paging:paging-runtime-ktx:3.2.0'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation "androidx.room:room-paging:2.5.2"
androidTestImplementation "androidx.room:room-testing:2.5.2"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation "androidx.compose.runtime:runtime:$composeVer"
androidTestImplementation 'com.google.truth:truth:1.1.5'
@ -172,7 +172,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
implementation "com.github.Irineu333:Highlight-KT:1.0.4"
// For media playback using ExoPlayer
@ -188,5 +188,5 @@ dependencies {
implementation "com.anggrayudi:storage:1.5.5"
implementation 'me.zhanghai.android.fastscroll:library:1.3.0'
implementation 'com.google.accompanist:accompanist-webview:0.30.1'
implementation 'androidx.compose.material3:material3-android:1.2.0-alpha08'
implementation 'androidx.compose.material3:material3-android:1.2.0-alpha09'
}

View file

@ -76,4 +76,17 @@
-dontwarn com.google.android.exoplayer2.**
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
-keep class com.deniscerri.** { *; }
-keep class org.apache.commons.compress.archivers.zip.** { *; }
# Keep `Companion` object fields of serializable classes.
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-dontobfuscate

View file

@ -4,10 +4,11 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@ -212,16 +213,6 @@
</intent-filter>
</activity>
<activity
android:name=".ui.ErrorDialogActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleInstance"
android:theme="@style/Theme.BottomSheet">
</activity>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"

View file

@ -45,6 +45,7 @@ import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.ui.more.settings.SettingsActivity
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.dialog.MaterialAlertDialogBuilder

View file

@ -47,6 +47,10 @@ abstract class DBManager : RoomDatabase(){
abstract val logDao: LogDao
abstract val terminalDao: TerminalDao
enum class SORTING{
DESC, ASC
}
companion object {
//prevents multiple instances of db getting created at the same time
@Volatile

View file

@ -2,11 +2,31 @@ package com.deniscerri.ytdlnis.database.dao
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import kotlinx.coroutines.flow.Flow
@Dao
interface CommandTemplateDao {
@Query("SELECT * FROM commandTemplates WHERE (title LIKE '%'||:query||'%' OR content LIKE '%'||:query||'%') ORDER BY " +
"CASE WHEN :sort = 'ASC' THEN id END ASC," +
"CASE WHEN :sort = 'DESC' THEN id END DESC," +
"CASE WHEN :sort = '' THEN id END DESC ")
fun getCommandsSortedByID(query : String, sort : String) : List<CommandTemplate>
@Query("SELECT * FROM commandTemplates WHERE (title LIKE '%'||:query||'%' OR content LIKE '%'||:query||'%') ORDER BY " +
"CASE WHEN :sort = 'ASC' THEN title COLLATE NOCASE END ASC," +
"CASE WHEN :sort = 'DESC' THEN title COLLATE NOCASE END DESC," +
"CASE WHEN :sort = '' THEN title COLLATE NOCASE END DESC ")
fun getCommandsSortedByTitle(query : String, sort : String) : List<CommandTemplate>
@Query("SELECT * FROM commandTemplates WHERE (title LIKE '%'||:query||'%' OR content LIKE '%'||:query||'%') ORDER BY " +
"CASE WHEN :sort = 'ASC' THEN length(content) END ASC," +
"CASE WHEN :sort = 'DESC' THEN length(content) END DESC," +
"CASE WHEN :sort = '' THEN length(content) END DESC ")
fun getCommandsSortedByContentLength(query : String, sort : String) : List<CommandTemplate>
@Query("SELECT * FROM commandTemplates ORDER BY id DESC")
fun getAllTemplates() : List<CommandTemplate>

View file

@ -33,7 +33,7 @@ interface HistoryDao {
fun getAllHistoryList() : List<HistoryItem>
@Query("SELECT * FROM history WHERE id=:id LIMIT 1")
suspend fun getHistoryItem(id: Int) : HistoryItem
fun getHistoryItem(id: Long) : HistoryItem
@Query("SELECT * FROM history WHERE url=:url")
fun getAllHistoryByURL(url: String) : List<HistoryItem>

View file

@ -1,8 +1,10 @@
package com.deniscerri.ytdlnis.database.repository
import androidx.lifecycle.LiveData
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import kotlinx.coroutines.flow.Flow
@ -10,10 +12,22 @@ class CommandTemplateRepository(private val commandDao: CommandTemplateDao) {
val items : Flow<List<CommandTemplate>> = commandDao.getAllTemplatesFlow()
val shortcuts : Flow<List<TemplateShortcut>> = commandDao.getAllShortcutsFlow()
enum class CommandTemplateSortType {
DATE, TITLE, LENGTH
}
fun getAll() : List<CommandTemplate> {
return commandDao.getAllTemplates()
}
fun getFiltered(query : String, sortType: CommandTemplateSortType, sort: DBManager.SORTING) : List<CommandTemplate> {
return when(sortType){
CommandTemplateSortType.DATE -> commandDao.getCommandsSortedByID(query, sort.toString())
CommandTemplateSortType.TITLE -> commandDao.getCommandsSortedByTitle(query, sort.toString())
CommandTemplateSortType.LENGTH -> commandDao.getCommandsSortedByContentLength(query, sort.toString())
}
}
fun getTotalNumber() : Int {
return commandDao.getTotalNumber()
}

View file

@ -1,23 +1,20 @@
package com.deniscerri.ytdlnis.database.repository
import androidx.lifecycle.LiveData
import com.deniscerri.ytdlnis.database.dao.HistoryDao
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.util.FileUtil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import com.deniscerri.ytdlnis.database.DBManager.SORTING
class HistoryRepository(private val historyDao: HistoryDao) {
val items : Flow<List<HistoryItem>> = historyDao.getAllHistory()
enum class HistorySort{
DESC, ASC
}
enum class HistorySortType {
DATE, TITLE, AUTHOR, FILESIZE
}
suspend fun getItem(id: Int) : HistoryItem {
fun getItem(id: Long) : HistoryItem {
return historyDao.getHistoryItem(id)
}
@ -25,7 +22,11 @@ class HistoryRepository(private val historyDao: HistoryDao) {
return historyDao.getAllHistoryList()
}
fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: HistorySort) : List<HistoryItem> {
fun getAllByURL(url: String) : List<HistoryItem> {
return historyDao.getAllHistoryByURL(url)
}
fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING) : List<HistoryItem> {
return when(sortType){
HistorySortType.DATE -> historyDao.getHistorySortedByID(query, format, site, sort.toString())
HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, format, site, sort.toString())
@ -33,8 +34,8 @@ class HistoryRepository(private val historyDao: HistoryDao) {
HistorySortType.FILESIZE -> {
val items = historyDao.getHistorySortedByID(query, format, site, sort.toString())
when(sort){
HistorySort.DESC -> items.sortedByDescending { it.format.filesize }
HistorySort.ASC -> items.sortedBy { it.format.filesize }
SORTING.DESC -> items.sortedByDescending { it.format.filesize }
SORTING.ASC -> items.sortedBy { it.format.filesize }
}
}
}
@ -44,8 +45,11 @@ class HistoryRepository(private val historyDao: HistoryDao) {
historyDao.insert(item)
}
suspend fun delete(item: HistoryItem){
suspend fun delete(item: HistoryItem, deleteFile: Boolean){
historyDao.delete(item.id)
if (deleteFile){
FileUtil.deleteFile(item.downloadPath)
}
}
suspend fun deleteAll(){

View file

@ -42,7 +42,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return res
}
suspend fun getOne(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{
suspend fun getYoutubeVideo(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?>{
val infoUtil = InfoUtil(context)
val v = infoUtil.getYoutubeVideo(inputQuery)
if (resetResults) {
@ -81,22 +81,17 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
suspend fun getDefault(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?> {
val infoUtil = InfoUtil(context)
try {
val items = infoUtil.getFromYTDL(inputQuery)
if (resetResults) {
deleteAll()
itemCount.value = items.size
}else{
items.forEach { it!!.playlistTitle = "ytdlnis-Search" }
}
items.forEach {
resultDao.insert(it!!)
}
return items
} catch (e: Exception) {
Log.e(tag, e.toString())
val items = infoUtil.getFromYTDL(inputQuery)
if (resetResults) {
deleteAll()
itemCount.value = items.size
}else{
items.forEach { it!!.playlistTitle = "ytdlnis-Search" }
}
return arrayListOf()
items.forEach {
resultDao.insert(it!!)
}
return items
}
suspend fun delete(item: ResultItem){

View file

@ -5,13 +5,17 @@ import android.content.ClipboardManager
import android.content.Context.CLIPBOARD_SERVICE
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.CommandTemplateExport
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import com.deniscerri.ytdlnis.database.repository.CommandTemplateRepository
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -21,15 +25,55 @@ import kotlinx.serialization.json.Json
class CommandTemplateViewModel(private val application: Application) : AndroidViewModel(application) {
private val repository: CommandTemplateRepository
val items: LiveData<List<CommandTemplate>>
val sortOrder = MutableLiveData(DBManager.SORTING.DESC)
val sortType = MutableLiveData(CommandTemplateRepository.CommandTemplateSortType.DATE)
private val queryFilter = MutableLiveData("")
private var _items = MediatorLiveData<List<CommandTemplate>>()
val allItems: LiveData<List<CommandTemplate>>
val shortcuts : LiveData<List<TemplateShortcut>>
private val jsonFormat = Json { prettyPrint = true }
init {
val dao = DBManager.getInstance(application).commandTemplateDao
repository = CommandTemplateRepository(dao)
items = repository.items.asLiveData()
allItems = repository.items.asLiveData()
shortcuts = repository.shortcuts.asLiveData()
_items.addSource(allItems){
filter(queryFilter.value!!, sortType.value!!, sortOrder.value!!)
}
_items.addSource(sortType){
filter(queryFilter.value!!, sortType.value!!, sortOrder.value!!)
}
_items.addSource(queryFilter){
filter(queryFilter.value!!, sortType.value!!, sortOrder.value!!)
}
}
fun getFilteredList() : LiveData<List<CommandTemplate>>{
return _items
}
fun setSorting(sort: CommandTemplateRepository.CommandTemplateSortType){
if (sortType.value != sort){
sortOrder.value = DBManager.SORTING.DESC
}else{
sortOrder.value = if (sortOrder.value == DBManager.SORTING.DESC) {
DBManager.SORTING.ASC
} else DBManager.SORTING.DESC
}
sortType.value = sort
}
private fun filter(query : String, sortType: CommandTemplateRepository.CommandTemplateSortType, sort: DBManager.SORTING) = viewModelScope.launch(Dispatchers.IO){
_items.postValue(repository.getFiltered(query, sortType, sort))
}
fun setQueryFilter(filter: String){
queryFilter.value = filter
}
fun getTemplate(itemId: Long): CommandTemplate {

View file

@ -2,17 +2,17 @@ package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.content.res.Resources
import android.net.ConnectivityManager
import android.os.Bundle
import android.os.Looper
import android.util.DisplayMetrics
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
@ -40,7 +40,7 @@ import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.models.VideoPreferences
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.ui.ErrorDialogActivity
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler
@ -49,8 +49,11 @@ import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Locale
import java.util.concurrent.TimeUnit
@ -85,7 +88,22 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
private var videoCodec: String?
private var audioCodec: String?
private val dao: DownloadDao
private val historyDao: HistoryDao
private val historyRepository: HistoryRepository
data class DownloadsUiState(
var errorMessage: Pair<Int, Int>?,
//action title, type, data
var actions: MutableList<Triple<Int, DownloadsAction, List<DownloadItem>?>>?
)
enum class DownloadsAction {
DOWNLOAD_ANYWAY
}
val uiState: MutableStateFlow<DownloadsUiState> = MutableStateFlow(DownloadsUiState(
errorMessage = null,
actions = null
))
enum class Type {
auto, audio, video, command
@ -100,8 +118,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
init {
dbManager = DBManager.getInstance(application)
dao = dbManager.downloadDao
historyDao = dbManager.historyDao
repository = DownloadRepository(dao)
historyRepository = HistoryRepository(dbManager.historyDao)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
infoUtil = InfoUtil(application)
@ -178,6 +196,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
return repository.getItemByID(id)
}
fun getHistoryItemById(id: Long) : HistoryItem? {
return historyRepository.getItem(id)
}
fun getDownloadType(t: Type? = null, url: String) : Type {
var type = t
@ -249,6 +271,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
}
if (preferredAudioFormats.isEmpty()){
preferredAudioFormats.add(getFormat(resultItem.formats, Type.audio).format_id)
}
val videoPreferences = VideoPreferences(
embedSubs,
addChapters, false,
@ -565,10 +591,12 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
startDownloadWorker(emptyList())
}
suspend fun queueDownloads(items: List<DownloadItem>) = CoroutineScope(Dispatchers.IO).launch {
suspend fun queueDownloads(items: List<DownloadItem>, ignoreDuplicate : Boolean = false) {
val context = App.instance
val alarmScheduler = AlarmScheduler(context)
val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads()
val activeAndQueuedDownloads = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloads()
}
val queuedItems = mutableListOf<DownloadItem>()
val existing = mutableListOf<DownloadItem>()
@ -578,30 +606,43 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
alarmScheduler.schedule()
}
items.forEach {
if (it.status != DownloadRepository.Status.Paused.toString()) it.status = DownloadRepository.Status.Queued.toString()
val currentCommand = infoUtil.buildYoutubeDLRequest(it)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
if (activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
} != null) {
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
}
if (existingDownload != null && !ignoreDuplicate) {
it.id = existingDownload.id
existing.add(it)
}else{
//check if downloaded and file exists
val history = historyDao.getAllHistoryByURL(it.url).filter { item -> FileUtil.exists(item.downloadPath) }
if (history.any { h -> h.command.replace("-P \"(.*?)\"".toRegex(), "") == parsedCurrentCommand.replace("-P \"(.*?)\"".toRegex(), "") }){
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> FileUtil.exists(item.downloadPath) }
}
val existingHistory = history.firstOrNull {
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
}
if (existingHistory != null && !ignoreDuplicate){
it.id = existingHistory.id
existing.add(it)
}else{
if (it.id == 0L){
val id = runBlocking {repository.insert(it)}
val id = withContext(Dispatchers.IO){
repository.insert(it)
}
it.id = id
}else if (it.status == DownloadRepository.Status.Queued.toString()){
repository.update(it)
withContext(Dispatchers.IO){
repository.update(it)
}
}
queuedItems.add(it)
@ -610,13 +651,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
if (existing.isNotEmpty()){
val intent = Intent(context, ErrorDialogActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.putExtra("title", context.resources.getString(R.string.download_already_exists))
intent.putExtra("message",
"${existing.size}/${items.size}\n" +
existing.mapIndexed { index, downloadItem -> "${index+1}. ${downloadItem.title}" }.joinToString("\n"))
context.startActivity(intent)
uiState.update { u -> u.copy(
errorMessage = Pair(R.string.download_already_exists, R.string.download_already_exists_summary),
actions = mutableListOf(
Triple(R.string.download, DownloadsAction.DOWNLOAD_ANYWAY, existing),
)
)
}
}
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || items.any { it.downloadStartTime > 0L } ){
@ -664,10 +705,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
.setConstraints(workConstraints.build())
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
if (delay > 1000L){
queuedItems.forEach {
workRequest.addTag(it.id.toString())
}
queuedItems.forEach {
workRequest.addTag(it.id.toString())
}
workManager.enqueueUniqueWork(

View file

@ -10,7 +10,7 @@ import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import com.deniscerri.ytdlnis.database.repository.HistoryRepository.HistorySort
import com.deniscerri.ytdlnis.database.DBManager.SORTING
import com.deniscerri.ytdlnis.database.repository.HistoryRepository.HistorySortType
import com.deniscerri.ytdlnis.util.FileUtil
import kotlinx.coroutines.Dispatchers
@ -18,7 +18,7 @@ import kotlinx.coroutines.launch
class HistoryViewModel(application: Application) : AndroidViewModel(application) {
private val repository : HistoryRepository
val sortOrder = MutableLiveData(HistorySort.DESC)
val sortOrder = MutableLiveData(SORTING.DESC)
val sortType = MutableLiveData(HistorySortType.DATE)
val websiteFilter = MutableLiveData("")
private val queryFilter = MutableLiveData("")
@ -55,11 +55,11 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
fun setSorting(sort: HistorySortType){
if (sortType.value != sort){
sortOrder.value = HistorySort.DESC
sortOrder.value = SORTING.DESC
}else{
sortOrder.value = if (sortOrder.value == HistorySort.DESC) {
HistorySort.ASC
} else HistorySort.DESC
sortOrder.value = if (sortOrder.value == SORTING.DESC) {
SORTING.ASC
} else SORTING.DESC
}
sortType.value = sort
}
@ -76,7 +76,7 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
formatFilter.value = filter
}
private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: HistorySort) = viewModelScope.launch(Dispatchers.IO){
private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING) = viewModelScope.launch(Dispatchers.IO){
_items.postValue(repository.getFiltered(query, format, site, sortType, sort))
}
@ -89,10 +89,7 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
}
fun delete(item: HistoryItem, deleteFile: Boolean) = viewModelScope.launch(Dispatchers.IO){
repository.delete(item)
if (deleteFile){
FileUtil.deleteFile(item.downloadPath)
}
repository.delete(item, deleteFile)
}
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {

View file

@ -17,7 +17,11 @@ import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.models.SearchHistoryItem
import com.deniscerri.ytdlnis.database.repository.ResultRepository
import com.deniscerri.ytdlnis.database.repository.SearchHistoryRepository
import com.deniscerri.ytdlnis.util.UiUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.regex.Pattern
@ -27,19 +31,28 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
val repository : ResultRepository
private val searchHistoryRepository : SearchHistoryRepository
val items : LiveData<List<ResultItem>>
val loadingItems = MutableLiveData<Boolean>()
private val sharedPreferences: SharedPreferences
var state: ResultsState = ResultsState.IDLE
enum class ResultsState {
PROCESSING, IDLE
data class ResultsUiState(
var processing: Boolean,
var errorMessage: Pair<Int, String>?,
var actions: MutableList<Pair<Int, ResultAction>>?
)
enum class ResultAction {
COPY_LOG
}
val uiState: MutableStateFlow<ResultsUiState> = MutableStateFlow(ResultsUiState(
processing = false,
errorMessage = null,
actions = null
))
private val sharedPreferences: SharedPreferences
init {
val dao = DBManager.getInstance(application).resultDao
repository = ResultRepository(dao, getApplication<Application>().applicationContext)
searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao)
items = repository.allResults.asLiveData()
loadingItems.postValue(false)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
}
@ -64,54 +77,38 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
deleteAll()
}
}
suspend fun parseQueries(inputQueries: List<String>) : MutableList<ResultItem?>{
state = ResultsState.PROCESSING
return if (inputQueries.size == 1){
parseQuery(inputQueries[0], true)
}else {
suspend fun parseQueries(inputQueries: List<String>) : List<ResultItem?> {
if (inputQueries.size > 1){
repository.itemCount.value = inputQueries.size
loadingItems.postValue(true)
val items = mutableListOf<ResultItem?>()
inputQueries.forEach {
items.addAll(parseQuery(it, false))
}
state = ResultsState.IDLE
loadingItems.postValue(false)
items
}
}
val resetResults = inputQueries.size == 1
suspend fun parseQuery(inputQuery: String, resetResults: Boolean) : ArrayList<ResultItem?> {
if (resetResults)
loadingItems.postValue(true)
val type = getQueryType(inputQuery)
var res = arrayListOf<ResultItem?>()
uiState.update {it.copy(processing = true, errorMessage = null, actions = null)}
return withContext(Dispatchers.IO){
try {
when (type) {
"Search" -> {
res = repository.search(inputQuery, resetResults)
}
"YT_Video" -> {
res = repository.getOne(inputQuery, resetResults)
}
"YT_Playlist" -> {
res = repository.getPlaylist(inputQuery, resetResults)
}
"Default" -> {
res = repository.getDefault(inputQuery, resetResults)
val res = mutableListOf<ResultItem?>()
inputQueries.forEach { inputQuery ->
val type = getQueryType(inputQuery)
try {
when (type) {
"Search" -> res.addAll(repository.search(inputQuery, resetResults))
"YT_Video" -> res.addAll(repository.getYoutubeVideo(inputQuery, resetResults))
"YT_Playlist" -> res.addAll(repository.getPlaylist(inputQuery, resetResults))
else -> res.addAll(repository.getDefault(inputQuery, resetResults))
}
} catch (e: Exception) {
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())
}
} catch (e: Exception) {
Log.e(tag, e.toString())
}
if (resetResults) {
state = ResultsState.IDLE
loadingItems.postValue(false)
}
uiState.update {it.copy(processing = false)}
res
}
}
private fun getQueryType(inputQuery: String) : String {
var type = "Search"
val p = Pattern.compile("(^(https?)://(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")

View file

@ -14,10 +14,7 @@ import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.util.Patterns
import android.view.Window
import android.view.WindowManager
import android.widget.TextView
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
@ -35,7 +32,6 @@ import com.deniscerri.ytdlnis.ui.BaseActivity
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.ui.downloadcard.SelectPlaylistItemsDialog
import com.deniscerri.ytdlnis.util.ThemeUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

View file

@ -1,59 +0,0 @@
package com.deniscerri.ytdlnis.ui
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.ThemeUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
open class ErrorDialogActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val title = intent.extras?.getString("title") ?: ""
val message = intent.extras?.getString("message") ?: ""
ThemeUtil.updateTheme(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v, insets ->
v.setPadding(0, 0, 0, 0)
insets
}
window.run {
setBackgroundDrawable(ColorDrawable(0))
setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
} else {
setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT)
}
}
val errDialog = MaterialAlertDialogBuilder(this)
.setTitle(title)
.setMessage(message)
.setIcon(R.drawable.ic_cancel)
.setPositiveButton(getString(R.string.copy_log)) { _: DialogInterface?, _: Int ->
val clipboard: ClipboardManager =
getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setText(message)
this.finish()
}.setOnDismissListener {
this.finish()
}
this.runOnUiThread {
errDialog.show()
}
super.onCreate(savedInstanceState)
}
}

View file

@ -40,6 +40,7 @@ import com.deniscerri.ytdlnis.ui.downloadcard.DownloadMultipleBottomSheetDialog
import com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.appbar.AppBarLayout
@ -54,6 +55,8 @@ import com.google.android.material.search.SearchBar
import com.google.android.material.search.SearchView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
@ -105,6 +108,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
activity = getActivity()
mainActivity = activity as MainActivity?
quickLaunchSheet = false
infoUtil = InfoUtil(requireContext())
return fragmentView
}
@ -136,6 +140,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
searchSuggestionsLinearLayout = view.findViewById(R.id.search_suggestions_linear_layout)
searchHistory = view.findViewById(R.id.search_history_scroll_view)
searchHistoryLinearLayout = view.findViewById(R.id.search_history_linear_layout)
homeFabs = view.findViewById(R.id.home_fabs)
downloadFabs = homeFabs!!.findViewById(R.id.download_selected_coordinator)
downloadAllFabCoordinator = homeFabs!!.findViewById(R.id.download_all_coordinator)
runCatching { materialToolbar!!.title = ThemeUtil.getStyledAppName(requireContext()) }
@ -174,30 +181,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
quickLaunchSheet = true
}
resultViewModel.loadingItems.observe(viewLifecycleOwner){
loadingItems = it
if (it){
recyclerView?.setPadding(0,0,0,0)
shimmerCards!!.startShimmer()
shimmerCards!!.visibility = VISIBLE
}else{
recyclerView?.setPadding(0,0,0,100)
shimmerCards!!.stopShimmer()
shimmerCards!!.visibility = GONE
if (resultsList!!.size > 1 && resultsList!![0]!!.playlistTitle.isNotEmpty()){
downloadAllFabCoordinator!!.visibility = VISIBLE
}else{
downloadAllFabCoordinator!!.visibility = GONE
}
}
}
initMenu()
homeFabs = view.findViewById(R.id.home_fabs)
downloadFabs = homeFabs!!.findViewById(R.id.download_selected_coordinator)
downloadAllFabCoordinator = homeFabs!!.findViewById(R.id.download_all_coordinator)
val downloadSelectedFab = downloadFabs!!.findViewById<ExtendedFloatingActionButton>(R.id.download_selected_fab)
downloadSelectedFab.tag = "downloadSelected"
downloadSelectedFab.setOnClickListener(this)
@ -239,15 +223,40 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}
}
lifecycleScope.launch {
launch{
resultViewModel.uiState.collectLatest { res ->
if (res.errorMessage != null){
kotlin.runCatching { UiUtil.handleResultResponse(requireActivity(), res) }
resultViewModel.uiState.update {it.copy(errorMessage = null, actions = null) }
}
loadingItems = res.processing
if (res.processing){
recyclerView?.setPadding(0,0,0,0)
shimmerCards!!.startShimmer()
shimmerCards!!.visibility = VISIBLE
}else{
recyclerView?.setPadding(0,0,0,100)
shimmerCards!!.stopShimmer()
shimmerCards!!.visibility = GONE
if (resultsList!!.size > 1 && resultsList!![0]!!.playlistTitle.isNotEmpty()){
downloadAllFabCoordinator!!.visibility = VISIBLE
}else{
downloadAllFabCoordinator!!.visibility = GONE
}
}
}
}
}
}
override fun onResume() {
super.onResume()
if(arguments?.getString("url") == null){
if (resultViewModel.state == ResultViewModel.ResultsState.IDLE){
if (!resultViewModel.uiState.value.processing){
resultViewModel.checkTrending()
}else{
resultViewModel.loadingItems.postValue(true)
}
}else{
arguments?.remove("url")
@ -302,7 +311,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}
infoUtil = InfoUtil(requireContext())
searchView!!.addTransitionListener { _, _, newState ->
if (newState == SearchView.TransitionState.SHOWN) {
val currentProvider = sharedPreferences?.getString("search_engine", "ytsearch")

View file

@ -30,6 +30,7 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.setTextAndRecalculateWidth
import com.deniscerri.ytdlnis.util.VideoPlayerUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -496,10 +497,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
}
chip.setOnLongClickListener {
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + chip.text + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
UiUtil.showGenericDeleteDialog(requireContext(), chip.text.toString(), accepted = {
player.seekTo(0)
player.pause()
chipGroup.removeView(chip)
@ -509,8 +507,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
player.stop()
dismiss()
}
}
deleteDialog.show()
})
true
}

View file

@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.ui.downloadcard
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
@ -11,11 +12,13 @@ import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isEmpty
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -52,6 +55,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
lateinit var downloadItem : DownloadItem
lateinit var title : TextInputLayout
lateinit var author : TextInputLayout
lateinit var preferences: SharedPreferences
lateinit var shownFields: List<String>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -63,6 +68,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
infoUtil = InfoUtil(requireContext())
genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources)
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
shownFields = preferences.getStringSet("modify_download_card", setOf())!!.toList()
return fragmentView
}
@ -82,6 +89,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
try {
title = view.findViewById(R.id.title_textinput)
title.visibility = if (shownFields.contains("title")) View.VISIBLE else View.GONE
if (title.editText?.text?.isEmpty() == true){
title.editText!!.setText(downloadItem.title)
}
@ -100,6 +108,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
}
author = view.findViewById(R.id.author_textinput)
author.visibility = if (shownFields.contains("author")) View.VISIBLE else View.GONE
if (author.editText?.text?.isEmpty() == true){
author.editText!!.setText(downloadItem.author)
}
@ -158,9 +167,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
if (currentDownloadItem!!.type != Type.audio){
downloadItem.type = Type.audio
runCatching {
downloadItem.format =
downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
.maxByOrNull { it.filesize }!!
downloadItem.format = downloadViewModel.getFormat(downloadItem.allFormats, Type.audio)
}.onFailure {
downloadItem.format = genericAudioFormats.last()
}
@ -172,6 +179,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
var containerPreference = sharedPreferences.getString("audio_format", "Default")
if (containerPreference == "Default") containerPreference = getString(R.string.defaultValue)
val container = view.findViewById<TextInputLayout>(R.id.downloadContainer)
container.visibility = if (shownFields.contains("container")) View.VISIBLE else View.GONE
val containerAutoCompleteTextView =
view.findViewById<AutoCompleteTextView>(R.id.container_textview)
@ -190,6 +198,10 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
resultItem?.formats?.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) })
if (resultItem != null){
resultViewModel.update(resultItem!!)
kotlin.runCatching {
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f1.updateUI(resultItem)
}
}
}
}
@ -229,44 +241,50 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
if (containers[index] == getString(R.string.defaultValue)) downloadItem.container = ""
}
UiUtil.configureAudio(
view,
requireActivity(),
listOf(downloadItem),
embedThumbClicked = {
downloadItem.audioPreferences.embedThumb = it
},
splitByChaptersClicked = {
downloadItem.audioPreferences.splitByChapters = it
},
filenameTemplateSet = {
downloadItem.customFileNameTemplate = it
},
sponsorBlockItemsSet = { values, checkedItems ->
downloadItem.audioPreferences.sponsorBlockFilters.clear()
for (i in checkedItems.indices) {
if (checkedItems[i]) {
downloadItem.audioPreferences.sponsorBlockFilters.add(values[i])
}
}
},
cutClicked = {cutVideoListener ->
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem?.urls ?: "", resultItem?.chapters ?: listOf(), cutVideoListener)
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
}
},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {
downloadItem.extraCommands = c
}
}
val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
view.findViewById<LinearLayout>(R.id.adjust).apply {
visibility = if (shownFields.contains("adjust_audio")) View.VISIBLE else View.GONE
if (isVisible){
UiUtil.configureAudio(
view,
requireActivity(),
listOf(downloadItem),
embedThumbClicked = {
downloadItem.audioPreferences.embedThumb = it
},
splitByChaptersClicked = {
downloadItem.audioPreferences.splitByChapters = it
},
filenameTemplateSet = {
downloadItem.customFileNameTemplate = it
},
sponsorBlockItemsSet = { values, checkedItems ->
downloadItem.audioPreferences.sponsorBlockFilters.clear()
for (i in checkedItems.indices) {
if (checkedItems[i]) {
downloadItem.audioPreferences.sponsorBlockFilters.add(values[i])
}
}
},
cutClicked = {cutVideoListener ->
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem?.urls ?: "", resultItem?.chapters ?: listOf(), cutVideoListener)
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
}
},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {
downloadItem.extraCommands = c
}
}
val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
}
)
}
)
}
}catch (e : Exception){
e.printStackTrace()
}

View file

@ -1,11 +1,8 @@
package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Bundle
@ -20,14 +17,12 @@ import android.widget.Button
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.edit
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
@ -45,9 +40,10 @@ 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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import java.text.SimpleDateFormat
import java.util.*
@ -205,6 +201,11 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
viewPager2.setPageTransformer(BackgroundToForegroundPageTransformer())
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
scheduleBtn.visibility = if(sharedPreferences.getStringSet("modify_download_card", setOf())!!.contains("schedule")){
View.VISIBLE
}else{
View.GONE
}
val download = view.findViewById<Button>(R.id.bottomsheet_download_button)
@ -249,6 +250,11 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
}
val link = view.findViewById<Button>(R.id.bottom_sheet_link)
link.visibility = if(sharedPreferences.getStringSet("modify_download_card", setOf())!!.contains("url")){
View.VISIBLE
}else{
View.GONE
}
val updateItem = view.findViewById<Button>(R.id.update_item)
if (Patterns.WEB_URL.matcher(result.url).matches()){
link.text = result.url
@ -282,6 +288,23 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
//update in the background if there is no data
if(result.title.isEmpty() && currentDownloadItem == null && !sharedPreferences.getBoolean("quick_download", false)){
initUpdateData(view)
}else {
val usingGenericFormatsOrEmpty = result.formats.isEmpty() || result.formats.any { it.format_note.contains("ytdlnisgeneric") }
if (usingGenericFormatsOrEmpty && sharedPreferences.getBoolean("update_formats", false)){
initUpdateFormats(result.url)
}
}
lifecycleScope.launch {
downloadViewModel.uiState.collectLatest { res ->
if (res.errorMessage != null) {
UiUtil.handleDownloadsResponse(requireActivity() as MainActivity, res, downloadViewModel)
downloadViewModel.uiState.value = DownloadViewModel.DownloadsUiState(
errorMessage = null,
actions = null
)
}
}
}
}
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{
@ -342,7 +365,7 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
return@launch
}
val result = resultViewModel.parseQuery(result.url, true)
val result = resultViewModel.parseQueries(listOf(result.url))
if (result.isEmpty()){
withContext(Dispatchers.Main){
Looper.prepare().run {
@ -376,6 +399,12 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
shimmerLoadingSubtitle.stopShimmer()
}
val usingGenericFormatsOrEmpty = result[0]!!.formats.isEmpty() || result[0]!!.formats.any { it.format_note.contains("ytdlnisgeneric") }
if (usingGenericFormatsOrEmpty && sharedPreferences.getBoolean("update_formats", false)){
initUpdateFormats(result[0]!!.url)
}
}else{
//open multi download card instead
if (activity is ShareActivity){
@ -394,6 +423,48 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
}
}
private fun initUpdateFormats(url: String){
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
withContext(Dispatchers.Main){
runCatching {
val f1 = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.VISIBLE
}
runCatching {
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.VISIBLE
}
}
val formats = infoUtil.getFormats(url).toMutableList()
withContext(Dispatchers.Main){
runCatching {
val f1 = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
val resultItem = downloadViewModel.createResultItemFromDownload(f1.downloadItem)
resultItem.formats = formats
fragmentAdapter.setResultItem(resultItem)
f1.updateUI(resultItem)
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.GONE
}
runCatching {
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
val resultItem = downloadViewModel.createResultItemFromDownload(f1.downloadItem)
resultItem.formats = formats
fragmentAdapter.setResultItem(resultItem)
f1.updateUI(resultItem)
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.GONE
}
}
if (formats.isNotEmpty()){
result.formats = formats
resultViewModel.update(result)
}
}
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
cleanUp()
@ -407,7 +478,7 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
private fun cleanUp(){
kotlin.runCatching {
repeat(parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.backStackEntryCount ?: 0){
repeat((parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.backStackEntryCount?.minus(1))?: 0){
parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.popBackStack()
}
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("downloadSingleSheet")!!).commit()

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipboardManager
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
@ -17,9 +18,11 @@ import android.widget.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.DownloadItem
@ -48,6 +51,8 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
private lateinit var commandTemplateViewModel : CommandTemplateViewModel
private lateinit var saveDir : TextInputLayout
private lateinit var freeSpace : TextView
private lateinit var preferences: SharedPreferences
private lateinit var shownFields: List<String>
lateinit var downloadItem: DownloadItem
@ -60,6 +65,8 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
activity = getActivity()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
shownFields = preferences.getStringSet("modify_download_card", setOf())!!.toList()
return fragmentView
}
@ -180,72 +187,76 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
commandTemplateViewModel.getTotalShortcutNumber()
}
UiUtil.configureCommand(
view,
if (downloadItem.url.isEmpty()) 0 else 1,
shortcutCount,
newTemplateClicked = {
UiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it)
chosenCommandView.editText!!.setText(it.content)
downloadItem.format = Format(
it.title,
"",
"",
"",
"",
0,
it.content
)
commandTemplateCard.visibility = View.VISIBLE
view.findViewById<TextView>(R.id.command_txt).visibility = View.VISIBLE
view.findViewById<Chip>(R.id.editSelected).isEnabled = true
populateCommandCard(commandTemplateCard, it)
}
},
editSelectedClicked =
{
var current = templates.find { it.id == commandTemplateCard.tag }
if (current == null) current =
CommandTemplate(
0,
"",
chosenCommandView.editText!!.text.toString(),
false
)
UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it)
chosenCommandView.editText!!.setText(it.content)
populateCommandCard(commandTemplateCard, it)
downloadItem.format = Format(
it.title,
"",
"",
"",
"",
0,
it.content
)
}
},
shortcutClicked = {
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel,
itemSelected = {
chosenCommandView.editText!!.setText("${chosenCommandView.editText!!.text} $it")
downloadItem.format.format_note = chosenCommandView.editText!!.text.toString()
},
itemRemoved = {removed ->
chosenCommandView.editText!!.setText(chosenCommandView.editText!!.text.replace("(${
Regex.escape(
removed
view.findViewById<LinearLayout>(R.id.adjust).apply {
visibility = if (shownFields.contains("adjust_command")) View.VISIBLE else View.GONE
if (isVisible){
UiUtil.configureCommand(
view,
if (downloadItem.url.isEmpty()) 0 else 1,
shortcutCount,
newTemplateClicked = {
UiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it)
chosenCommandView.editText!!.setText(it.content)
downloadItem.format = Format(
it.title,
"",
"",
"",
"",
0,
it.content
)
})(?!.*\\1)".toRegex(), "").trimEnd())
downloadItem.format.format_note = chosenCommandView.editText!!.text.toString().trimEnd()
})
commandTemplateCard.visibility = View.VISIBLE
view.findViewById<TextView>(R.id.command_txt).visibility = View.VISIBLE
view.findViewById<Chip>(R.id.editSelected).isEnabled = true
populateCommandCard(commandTemplateCard, it)
}
},
editSelectedClicked =
{
var current = templates.find { it.id == commandTemplateCard.tag }
if (current == null) current =
CommandTemplate(
0,
"",
chosenCommandView.editText!!.text.toString(),
false
)
UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it)
chosenCommandView.editText!!.setText(it.content)
populateCommandCard(commandTemplateCard, it)
downloadItem.format = Format(
it.title,
"",
"",
"",
"",
0,
it.content
)
}
},
shortcutClicked = {
UiUtil.showShortcuts(requireActivity(), commandTemplateViewModel,
itemSelected = {
chosenCommandView.editText!!.setText("${chosenCommandView.editText!!.text} $it")
downloadItem.format.format_note = chosenCommandView.editText!!.text.toString()
},
itemRemoved = {removed ->
chosenCommandView.editText!!.setText(chosenCommandView.editText!!.text.replace("(${
Regex.escape(
removed
)
})(?!.*\\1)".toRegex(), "").trimEnd())
downloadItem.format.format_note = chosenCommandView.editText!!.text.toString().trimEnd()
})
}
)
}
)
}
} catch (e: Exception) {
e.printStackTrace()
}

View file

@ -32,6 +32,7 @@ import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
@ -55,9 +56,8 @@ import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@ -506,6 +506,18 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
true
}
lifecycleScope.launch {
downloadViewModel.uiState.collectLatest { res ->
if (res.errorMessage != null) {
UiUtil.handleDownloadsResponse(requireActivity() as MainActivity, res, downloadViewModel)
downloadViewModel.uiState.value = DownloadViewModel.DownloadsUiState(
errorMessage = null,
actions = null
)
}
}
}
}
private fun updateFileSize(items: List<Long>){

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
@ -13,9 +14,11 @@ import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -44,7 +47,8 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
private lateinit var resultViewModel: ResultViewModel
private lateinit var preferences: SharedPreferences
private lateinit var shownFields: List<String>
lateinit var title : TextInputLayout
lateinit var author : TextInputLayout
private lateinit var saveDir : TextInputLayout
@ -54,6 +58,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
lateinit var downloadItem: DownloadItem
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -65,6 +70,8 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
resultViewModel = ViewModelProvider(this@DownloadVideoFragment)[ResultViewModel::class.java]
infoUtil = InfoUtil(requireContext())
genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources)
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
shownFields = preferences.getStringSet("modify_download_card", setOf())!!.toList()
return fragmentView
}
@ -85,6 +92,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
try {
title = view.findViewById(R.id.title_textinput)
title.visibility = if (shownFields.contains("title")) View.VISIBLE else View.GONE
if (title.editText?.text?.isEmpty() == true){
title.editText!!.setText(downloadItem.title)
}
@ -97,6 +105,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
})
author = view.findViewById(R.id.author_textinput)
author.visibility = if (shownFields.contains("author")) View.VISIBLE else View.GONE
if (author.editText?.text?.isEmpty() == true){
author.editText!!.setText(downloadItem.author)
}
@ -149,9 +158,12 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
if (currentDownloadItem!!.type != Type.video){
downloadItem.type = Type.video
runCatching {
downloadItem.format =
downloadItem.allFormats.filter { it.vcodec.isNotEmpty() }
.maxByOrNull { it.filesize }!!
downloadItem.format = downloadViewModel.getFormat(downloadItem.allFormats, Type.video)
if (downloadItem.videoPreferences.audioFormatIDs.isEmpty()){
downloadItem.videoPreferences.audioFormatIDs.add(
downloadViewModel.getFormat(downloadItem.allFormats, Type.audio).format_id
)
}
}.onFailure {
downloadItem.format = genericVideoFormats.last()
}
@ -161,6 +173,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
val containers = requireContext().resources.getStringArray(R.array.video_containers)
val container = view.findViewById<TextInputLayout>(R.id.downloadContainer)
container.visibility = if (shownFields.contains("container")) View.VISIBLE else View.GONE
val containerAutoCompleteTextView =
view.findViewById<AutoCompleteTextView>(R.id.container_textview)
var containerPreference = sharedPreferences.getString("video_format", "Default")
@ -185,6 +198,10 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
resultItem?.formats?.addAll(allFormats.first().filter { !genericVideoFormats.contains(it) })
if (resultItem != null){
resultViewModel.update(resultItem!!)
kotlin.runCatching {
val f1 = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f1.updateUI(resultItem)
}
}
}
}
@ -226,61 +243,66 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
}
UiUtil.configureVideo(
view,
requireActivity(),
listOf(downloadItem),
embedSubsClicked = {
downloadItem.videoPreferences.embedSubs = it
},
addChaptersClicked = {
downloadItem.videoPreferences.addChapters = it
},
splitByChaptersClicked = {
downloadItem.videoPreferences.splitByChapters = it
},
saveThumbnailClicked = {
downloadItem.SaveThumb = it
},
sponsorBlockItemsSet = { values, checkedItems ->
downloadItem.videoPreferences.sponsorBlockFilters.clear()
for (i in checkedItems.indices) {
if (checkedItems[i]) {
downloadItem.videoPreferences.sponsorBlockFilters.add(values[i])
}
}
},
cutClicked = { cutVideoListener ->
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem?.urls ?: "", resultItem?.chapters ?: listOf(), cutVideoListener)
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
}
},
filenameTemplateSet = {
downloadItem.customFileNameTemplate = it
},
saveSubtitlesClicked = {
downloadItem.videoPreferences.writeSubs = it
},
subtitleLanguagesClicked = {
UiUtil.showSubtitleLanguagesDialog(requireActivity(), downloadItem.videoPreferences.subsLanguages){
downloadItem.videoPreferences.subsLanguages = it
}
},
removeAudioClicked = {
downloadItem.videoPreferences.removeAudio = it
},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {
downloadItem.extraCommands = c
}
}
view.findViewById<LinearLayout>(R.id.adjust).apply {
visibility = if (shownFields.contains("adjust_video")) View.VISIBLE else View.GONE
if (isVisible){
UiUtil.configureVideo(
view,
requireActivity(),
listOf(downloadItem),
embedSubsClicked = {
downloadItem.videoPreferences.embedSubs = it
},
addChaptersClicked = {
downloadItem.videoPreferences.addChapters = it
},
splitByChaptersClicked = {
downloadItem.videoPreferences.splitByChapters = it
},
saveThumbnailClicked = {
downloadItem.SaveThumb = it
},
sponsorBlockItemsSet = { values, checkedItems ->
downloadItem.videoPreferences.sponsorBlockFilters.clear()
for (i in checkedItems.indices) {
if (checkedItems[i]) {
downloadItem.videoPreferences.sponsorBlockFilters.add(values[i])
}
}
},
cutClicked = { cutVideoListener ->
if (parentFragmentManager.findFragmentByTag("cutVideoSheet") == null){
val bottomSheet = CutVideoBottomSheetDialog(downloadItem, resultItem?.urls ?: "", resultItem?.chapters ?: listOf(), cutVideoListener)
bottomSheet.show(parentFragmentManager, "cutVideoSheet")
}
},
filenameTemplateSet = {
downloadItem.customFileNameTemplate = it
},
saveSubtitlesClicked = {
downloadItem.videoPreferences.writeSubs = it
},
subtitleLanguagesClicked = {
UiUtil.showSubtitleLanguagesDialog(requireActivity(), downloadItem.videoPreferences.subsLanguages){
downloadItem.videoPreferences.subsLanguages = it
}
},
removeAudioClicked = {
downloadItem.videoPreferences.removeAudio = it
},
extraCommandsClicked = {
val callback = object : ExtraCommandsListener {
override fun onChangeExtraCommand(c: String) {
downloadItem.extraCommands = c
}
}
val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
val bottomSheetDialog = AddExtraCommandsDialog(downloadItem, callback)
bottomSheetDialog.show(parentFragmentManager, "extraCommands")
}
)
}
)
}
} catch (e: Exception) {
e.printStackTrace()
}

View file

@ -10,7 +10,6 @@ import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -28,7 +27,6 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext

View file

@ -55,7 +55,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
fragmentView = inflater.inflate(R.layout.generic_recyclerview, container, false)
activity = getActivity()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
return fragmentView
@ -100,7 +100,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
downloadViewModel.getItemByID(itemID)
}
withContext(Dispatchers.IO){
downloadViewModel.queueDownloads(listOf(item))
downloadViewModel.queueDownloads(listOf(item), true)
}
}.onFailure {
Toast.makeText(requireContext(), getString(R.string.error_restarting_download), Toast.LENGTH_LONG).show()
@ -264,6 +264,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val itemID = viewHolder.itemView.tag.toString().toLong()
val position = viewHolder.bindingAdapterPosition
when (direction) {
ItemTouchHelper.LEFT -> {
lifecycleScope.launch {
@ -279,6 +280,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic
}
ItemTouchHelper.RIGHT -> {
onActionButtonClick(itemID)
adapter.notifyItemChanged(position)
}
}
}

View file

@ -56,7 +56,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
fragmentView = inflater.inflate(R.layout.generic_recyclerview, container, false)
activity = getActivity()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
return fragmentView
@ -269,10 +269,15 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val itemID = viewHolder.itemView.tag.toString().toLong()
val position = viewHolder.bindingAdapterPosition
when (direction) {
ItemTouchHelper.RIGHT -> {
runBlocking{
downloadViewModel.reQueueDownloadItems(listOf(itemID))
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
downloadViewModel.queueDownloads(listOf(item), true)
adapter.notifyItemChanged(position)
}
}
ItemTouchHelper.LEFT -> {

View file

@ -19,7 +19,6 @@ import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
@ -41,28 +40,22 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.HistoryAdapter
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import com.deniscerri.ytdlnis.database.repository.HistoryRepository.HistorySort
import com.deniscerri.ytdlnis.database.DBManager.SORTING
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHistoryBinding
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.runBlocking
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
/**
* A fragment representing a list of Items.
@ -163,8 +156,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyViewModel.sortOrder.observe(viewLifecycleOwner){
if (it != null){
when(it){
HistorySort.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down)
HistorySort.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up)
SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down)
SORTING.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up)
}
}
}
@ -277,12 +270,12 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
}
}
private fun changeSortIcon(item: TextView, order: HistorySort){
private fun changeSortIcon(item: TextView, order: SORTING){
when(order){
HistorySort.DESC ->{
SORTING.DESC ->{
item.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_up, 0,0,0)
}
HistorySort.ASC -> {
SORTING.ASC -> {
item.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_down, 0,0,0)
}
}
@ -405,146 +398,23 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
requireView().findViewById<View>(R.id.website_divider).visibility = VISIBLE
}
private fun removeItem(item: HistoryItem) {
if (bottomSheet != null) bottomSheet!!.hide()
val deleteFile = booleanArrayOf(false)
val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!)
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
val path = item.downloadPath
val file = File(path)
if (file.exists() && path.isNotEmpty()) {
deleteDialog.setMultiChoiceItems(
arrayOf(getString(R.string.delete_file_too)),
booleanArrayOf(false)
) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b }
}
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
historyViewModel.delete(item, deleteFile[0])
}
deleteDialog.show()
}
override fun onCardClick(itemID: Long, isPresent: Boolean) {
bottomSheet = BottomSheetDialog(fragmentContext!!)
bottomSheet!!.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet!!.setContentView(R.layout.history_item_details_bottom_sheet)
val item = findItem(itemID)
val title = bottomSheet!!.findViewById<TextView>(R.id.bottom_sheet_title)
title!!.text = item!!.title
val author = bottomSheet!!.findViewById<TextView>(R.id.bottom_sheet_author)
author!!.text = item.author
// BUTTON ----------------------------------
val btn = bottomSheet!!.findViewById<MaterialButton>(R.id.downloads_download_button_type)
if (item.type == DownloadViewModel.Type.audio) {
if (isPresent) btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_music_downloaded) else btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_music)
} else if (item.type == DownloadViewModel.Type.video) {
if (isPresent) btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video_downloaded) else btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video)
}else{
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal)
}
if (isPresent){
btn.setOnClickListener {
UiUtil!!.shareFileIntent(requireContext(), listOf(item.downloadPath))
UiUtil.showHistoryItemDetailsCard(item, requireActivity(), isPresent,
redownloadItem = {
val downloadItem = downloadViewModel.createDownloadItemFromHistory(it)
runBlocking{
downloadViewModel.queueDownloads(listOf(downloadItem))
}
historyViewModel.delete(it, false)
}
}
val time = bottomSheet!!.findViewById<TextView>(R.id.time)
val formatNote = bottomSheet!!.findViewById<TextView>(R.id.format_note)
val container = bottomSheet!!.findViewById<TextView>(R.id.container_chip)
val codec = bottomSheet!!.findViewById<TextView>(R.id.codec)
val fileSize = bottomSheet!!.findViewById<TextView>(R.id.file_size)
val calendar = Calendar.getInstance()
calendar.timeInMillis = item.time * 1000L
time!!.text = SimpleDateFormat(android.text.format.DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(calendar.time)
time.isClickable = false
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility = GONE
else formatNote!!.text = item.format.format_note
if (item.format.container != "") container!!.text = item.format.container.uppercase()
else container!!.visibility = GONE
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 = GONE
}else{
codec!!.visibility = VISIBLE
codec.text = codecText
}
val file = File(item.downloadPath)
val fileSizeReadable = FileUtil.convertFileSize(if (file.exists()) file.length() else item.format.filesize)
if (fileSizeReadable == "?") fileSize!!.visibility = GONE
else fileSize!!.text = fileSizeReadable
val link = bottomSheet!!.findViewById<Button>(R.id.bottom_sheet_link)
val url = item.url
link!!.text = url
link.tag = itemID
link.setOnClickListener{
UiUtil.openLinkIntent(requireContext(), item.url, bottomSheet)
}
link.setOnLongClickListener{
UiUtil.copyLinkToClipBoard(requireContext(), item.url, bottomSheet)
true
}
val remove = bottomSheet!!.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = itemID
remove.setOnClickListener{
removeItem(item)
}
val openFile = bottomSheet!!.findViewById<Button>(R.id.bottomsheet_open_file_button)
openFile!!.tag = itemID
openFile.setOnClickListener{
UiUtil.openFileIntent(requireContext(), item.downloadPath)
}
val redownload = bottomSheet!!.findViewById<Button>(R.id.bottomsheet_redownload_button)
redownload!!.tag = itemID
redownload.setOnClickListener{
val downloadItem = downloadViewModel.createDownloadItemFromHistory(item)
runBlocking{
downloadViewModel.queueDownloads(listOf(downloadItem))
}
historyViewModel.delete(item, false)
bottomSheet!!.cancel()
}
redownload.setOnLongClickListener {
bottomSheet!!.cancel()
) {
val sheet = DownloadBottomSheetDialog(
result = downloadViewModel.createResultItemFromHistory(item),
type = item.type)
result = downloadViewModel.createResultItemFromHistory(it),
type = it.type
)
sheet.show(parentFragmentManager, "downloadSingleSheet")
true
}
if (!isPresent) openFile.visibility = GONE
else redownload.visibility = GONE
bottomSheet!!.show()
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet!!.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet!!.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
override fun onCardSelect(itemID: Long, isChecked: Boolean) {
@ -666,7 +536,10 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
ItemTouchHelper.LEFT -> {
val deletedItem = historyList!![position]
historyAdapter!!.notifyItemChanged(position)
removeItem(deletedItem!!)
UiUtil.showRemoveHistoryItemDialog(deletedItem!!, requireActivity(),
delete = { item, deleteFile ->
historyViewModel.delete(item, deleteFile)
})
}
ItemTouchHelper.RIGHT -> {
val item = historyList!![position]!!

View file

@ -54,7 +54,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
fragmentView = inflater.inflate(R.layout.generic_recyclerview, container, false)
activity = getActivity()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
return fragmentView

View file

@ -4,11 +4,19 @@ import android.content.DialogInterface
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.InputType
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -21,9 +29,12 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.TemplatesAdapter
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.database.repository.CommandTemplateRepository.CommandTemplateSortType
import com.deniscerri.ytdlnis.database.DBManager.SORTING
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
@ -41,6 +52,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
private lateinit var templatesList: List<CommandTemplate>
private lateinit var noResults: RelativeLayout
private lateinit var mainActivity: MainActivity
private lateinit var sortChip: Chip
override fun onCreateView(
inflater: LayoutInflater,
@ -58,6 +70,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
topAppBar = view.findViewById(R.id.logs_toolbar)
topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() }
noResults = view.findViewById(R.id.no_results)
sortChip = view.findViewById(R.id.sortChip)
templatesAdapter =
TemplatesAdapter(
@ -75,35 +88,90 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
commandTemplateViewModel.items.observe(viewLifecycleOwner) {
commandTemplateViewModel.allItems.observe(viewLifecycleOwner) {
if (it.isEmpty()) noResults.visibility = View.VISIBLE
else noResults.visibility = View.GONE
templatesList = it
templatesAdapter.submitList(it)
}
commandTemplateViewModel.getFilteredList().observe(viewLifecycleOwner) {
templatesAdapter.submitList(it)
templatesList = it
scrollToTop()
}
commandTemplateViewModel.sortOrder.observe(viewLifecycleOwner){
if (it != null){
when(it){
SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down)
SORTING.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up)
}
}
}
commandTemplateViewModel.sortType.observe(viewLifecycleOwner){
if(it != null){
when(it){
CommandTemplateSortType.DATE -> sortChip.text = getString(R.string.date_added)
CommandTemplateSortType.TITLE -> sortChip.text = getString(R.string.title)
CommandTemplateSortType.LENGTH -> sortChip.text = getString(R.string.length)
}
}
}
initMenu()
initChips()
}
private fun initMenu() {
topAppBar.setOnMenuItemClickListener { m: MenuItem ->
val itemId = m.itemId
if (itemId == R.id.export_clipboard) {
lifecycleScope.launch{
withContext(Dispatchers.IO){
commandTemplateViewModel.exportToClipboard()
}
Snackbar.make(recyclerView, getString(R.string.copied_to_clipboard), Snackbar.LENGTH_LONG).show()
val onActionExpandListener: MenuItem.OnActionExpandListener =
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean {
return true
}
}else if (itemId == R.id.import_clipboard){
lifecycleScope.launch{
withContext(Dispatchers.IO){
val count = commandTemplateViewModel.importFromClipboard()
mainActivity.runOnUiThread{
Snackbar.make(recyclerView, "${getString(R.string.items_imported)} (${count})", Snackbar.LENGTH_LONG).show()
}
}
override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean {
return true
}
}
topAppBar.menu.findItem(R.id.search_command)
.setOnActionExpandListener(onActionExpandListener)
val searchView = topAppBar.menu.findItem(R.id.search_command).actionView as SearchView?
searchView!!.inputType = InputType.TYPE_CLASS_TEXT
searchView.queryHint = getString(R.string.search_command_hint)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
topAppBar.menu.findItem(R.id.search_command).collapseActionView()
commandTemplateViewModel.setQueryFilter(query)
return true
}
override fun onQueryTextChange(newText: String): Boolean {
commandTemplateViewModel.setQueryFilter(newText)
return true
}
})
topAppBar.setOnClickListener { scrollToTop() }
topAppBar.setOnMenuItemClickListener { m: MenuItem ->
when(m.itemId){
R.id.export_clipboard -> {
lifecycleScope.launch{
withContext(Dispatchers.IO){
commandTemplateViewModel.exportToClipboard()
}
Snackbar.make(recyclerView, getString(R.string.copied_to_clipboard), Snackbar.LENGTH_LONG).show()
}
}
R.id.import_clipboard -> {
lifecycleScope.launch{
withContext(Dispatchers.IO){
val count = commandTemplateViewModel.importFromClipboard()
mainActivity.runOnUiThread{
Snackbar.make(recyclerView, "${getString(R.string.items_imported)} (${count})", Snackbar.LENGTH_LONG).show()
}
}
}
}
}
true
@ -111,6 +179,45 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
}
private fun initChips() {
val sorting = view?.findViewById<Chip>(R.id.sortChip)
sorting?.setOnClickListener {
val sortSheet = BottomSheetDialog(requireContext())
sortSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
sortSheet.setContentView(R.layout.command_templates_sort_sheet)
val date = sortSheet.findViewById<TextView>(R.id.date)
val title = sortSheet.findViewById<TextView>(R.id.title)
val length = sortSheet.findViewById<TextView>(R.id.length)
val sortOptions = listOf(date!!, title!!, length!!)
sortOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
when(commandTemplateViewModel.sortType.value!!) {
CommandTemplateSortType.DATE -> changeSortIcon(date, commandTemplateViewModel.sortOrder.value!!)
CommandTemplateSortType.TITLE -> changeSortIcon(title, commandTemplateViewModel.sortOrder.value!!)
CommandTemplateSortType.LENGTH -> changeSortIcon(length, commandTemplateViewModel.sortOrder.value!!)
}
date.setOnClickListener {
sortOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
commandTemplateViewModel.setSorting(CommandTemplateSortType.DATE)
changeSortIcon(date, commandTemplateViewModel.sortOrder.value!!)
}
title.setOnClickListener {
sortOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
commandTemplateViewModel.setSorting(CommandTemplateSortType.TITLE)
changeSortIcon(title, commandTemplateViewModel.sortOrder.value!!)
}
length.setOnClickListener {
sortOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
commandTemplateViewModel.setSorting(CommandTemplateSortType.LENGTH)
changeSortIcon(length, commandTemplateViewModel.sortOrder.value!!)
}
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
sortSheet.behavior.peekHeight = displayMetrics.heightPixels
sortSheet.show()
}
val new = view?.findViewById<Chip>(R.id.newTemplate)
new?.setOnClickListener {
UiUtil.showCommandTemplateCreationOrUpdatingSheet(null,mainActivity, this, commandTemplateViewModel) {}
@ -121,8 +228,25 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
}
}
companion object {
private const val TAG = "DownloadLogActivity"
private fun changeSortIcon(item: TextView, order: SORTING){
when(order){
SORTING.DESC ->{
item.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_up, 0,0,0)
}
SORTING.ASC -> {
item.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_down, 0,0,0)
}
}
}
private fun scrollToTop() {
recyclerView.scrollToPosition(0)
Handler(Looper.getMainLooper()).post {
(topAppBar.parent as AppBarLayout).setExpanded(
true,
true
)
}
}
override fun onItemClick(commandTemplate: CommandTemplate, index: Int) {

View file

@ -62,15 +62,6 @@ class MoreFragment : Fragment() {
terminateApp = view.findViewById(R.id.terminate)
settings = view.findViewById(R.id.settings)
val navHostFragment = parentFragmentManager.findFragmentById(R.id.frame_layout)
if (mainSharedPreferences.getBoolean("log_downloads", false)) {
logs.visibility = View.VISIBLE
}else {
logs.visibility = View.GONE
}
terminal.setOnClickListener {
val intent = Intent(context, TerminalActivity::class.java)
startActivity(intent)

View file

@ -55,7 +55,6 @@ class WebViewActivity : BaseActivity() {
generateBtn = toolbar.findViewById(R.id.generate)
webViewCompose = findViewById(R.id.webview_compose)
cookieManager = CookieManager.getInstance()
cookieManager.removeAllCookies(null)
preferences = PreferenceManager.getDefaultSharedPreferences(this)
webViewClient = object : AccompanistWebViewClient() {
@ -94,7 +93,6 @@ class WebViewActivity : BaseActivity() {
Toast.makeText(this, "Something went wrong", Toast.LENGTH_SHORT).show()
}
}
cookieManager.removeAllCookies(null)
onBackPressedDispatcher.onBackPressed()
}

View file

@ -17,6 +17,7 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.work.MoveCacheFilesWorker
import com.google.android.material.snackbar.Snackbar
import java.io.File
@ -30,8 +31,8 @@ class FolderSettingsFragment : BaseSettingsFragment() {
private var commandPath: Preference? = null
private var accessAllFiles : Preference? = null
private var cacheDownloads : Preference? = null
private var audioFilenameTemplate : EditTextPreference? = null
private var videoFilenameTemplate : EditTextPreference? = null
private var audioFilenameTemplate : Preference? = null
private var videoFilenameTemplate : Preference? = null
private var clearCache: Preference? = null
private var moveCache: Preference? = null
@ -68,7 +69,7 @@ class FolderSettingsFragment : BaseSettingsFragment() {
accessAllFiles!!.isVisible = false
cacheDownloads!!.isEnabled = true
}else{
editor.putBoolean("cache_downloads", false).apply()
editor.putBoolean("cache_downloads", true).apply()
cacheDownloads!!.isEnabled = false
}
@ -109,15 +110,30 @@ class FolderSettingsFragment : BaseSettingsFragment() {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = Uri.parse("package:" + requireContext().packageName)
intent.data = uri
startActivity(intent)
accessAllFilesLauncher.launch(intent)
true
}
videoFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
videoFilenameTemplate?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
videoFilenameTemplate?.summary = preferences.getString("file_name_template", "%(uploader)s - %(title)s")
audioFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]"
audioFilenameTemplate?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]"
audioFilenameTemplate?.summary = preferences.getString("file_name_template_audio", "%(uploader)s - %(title)s")
videoFilenameTemplate?.setOnPreferenceClickListener {
UiUtil.showFilenameTemplateDialog(requireActivity(), videoFilenameTemplate?.summary.toString() ?: "", "${getString(R.string.file_name_template)} [${getString(R.string.video)}]") {
editor.putString("file_name_template", it).apply()
videoFilenameTemplate?.summary = it
}
false
}
audioFilenameTemplate?.setOnPreferenceClickListener {
UiUtil.showFilenameTemplateDialog(requireActivity(), audioFilenameTemplate?.summary.toString() ?: "", "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]") {
editor.putString("file_name_template_audio", it).apply()
audioFilenameTemplate?.summary = it
}
false
}
var 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)}"
@ -207,6 +223,15 @@ class FolderSettingsFragment : BaseSettingsFragment() {
}
}
private var accessAllFilesLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
accessAllFiles?.isVisible = false
cacheDownloads?.isEnabled = true
}
}
private fun changePath(p: Preference?, data: Intent?, requestCode: Int) {
val path = data!!.data.toString()
p!!.summary = FileUtil.formatPath(data.data.toString())

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@ -8,6 +9,7 @@ import android.net.Uri
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
@ -32,6 +34,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
private var highContrast: SwitchPreferenceCompat? = null
private var locale: ListPreference? = null
private var showTerminalShareIcon: SwitchPreferenceCompat? = null
private var ignoreBatteryOptimization: Preference? = null
private var updateUtil: UpdateUtil? = null
private var activeDownloadCount = 0
@ -127,7 +130,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
true
}
var ignoreBatteryOptimization = findPreference<Preference>("ignore_battery")
ignoreBatteryOptimization = findPreference<Preference>("ignore_battery")
val packageName: String = requireContext().packageName
val pm = requireContext().applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
if (pm.isIgnoringBatteryOptimizations(packageName)) {
@ -140,8 +143,16 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
val intent = Intent()
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:" + requireContext().packageName)
startActivity(intent)
ignoreBatteryLauncher.launch(intent)
true
}
}
private var ignoreBatteryLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
ignoreBatteryOptimization?.isVisible = false
}
}
}

View file

@ -8,6 +8,7 @@ import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.util.LayoutDirection
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
@ -171,7 +172,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS), "YTDLnis_${BuildConfig.VERSION_NAME}_${currentTime.get(
Calendar.YEAR)}-${currentTime.get(Calendar.MONTH) + 1}-${currentTime.get(
Calendar.DAY_OF_MONTH)}.json")
Calendar.DAY_OF_MONTH)} [${currentTime.get(Calendar.MILLISECOND)}.json")
saveFile.delete()
saveFile.createNewFile()
saveFile.writeText(Gson().toJson(json))
@ -223,15 +224,14 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
private fun backupSettings(preferences: SharedPreferences) : JsonArray {
runCatching {
val prefs = preferences.all
prefs.remove("app_language")
val arr = JsonArray()
prefs.forEach {
if (it.key != "app_language"){
val obj = JsonObject()
obj.addProperty("key", it.key)
obj.addProperty("value", it.value.toString())
obj.addProperty("type", it.value!!::class.simpleName)
arr.add(obj)
}
val obj = JsonObject()
obj.addProperty("key", it.key)
obj.addProperty("value", it.value.toString())
obj.addProperty("type", it.value!!::class.simpleName)
arr.add(obj)
}
return arr
}
@ -398,27 +398,26 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit(commit = true){
clear()
val prefs = json.getAsJsonArray("settings")
val preferencesKeys = preferences.all.map { it.key }
prefs.forEach {
val key : String = it.asJsonObject.get("key").toString().replace("\"", "")
if (preferencesKeys.contains(key)){
when(it.asJsonObject.get("type").toString().replace("\"", "")){
"String" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "")
putString(key, value)
}
"Boolean" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
putBoolean(key, value)
}
"Int" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
putInt(key, value)
}
"HashSet" -> {
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",")
putStringSet(key, value.toHashSet())
}
when(it.asJsonObject.get("type").toString().replace("\"", "")){
"String" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "")
putString(key, value)
}
"Boolean" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
Log.e("REST", value.toString())
Log.e("REST", key)
putBoolean(key, value)
}
"Int" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
putInt(key, value)
}
"HashSet" -> {
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",")
putStringSet(key, value.toHashSet())
}
}
}

View file

@ -1,7 +1,6 @@
package com.deniscerri.ytdlnis.util
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
import android.os.Looper
@ -16,7 +15,6 @@ import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.ErrorDialogActivity
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL
@ -387,17 +385,13 @@ class InfoUtil(private val context: Context) {
val items = arrayListOf<ResultItem?>()
val url = "$pipedURL/trending?region=$countryCODE"
val res = genericArrayRequest(url)
try {
for (i in 0 until res.length()) {
val element = res.getJSONObject(i)
if (element.getInt("duration") < 0) continue
element.put("uploader", element.getString("uploaderName"))
val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
if (v == null || v.thumb.isEmpty()) continue
items.add(v)
}
} catch (e: Exception) {
e.printStackTrace()
for (i in 0 until res.length()) {
val element = res.getJSONObject(i)
if (element.getInt("duration") < 0) continue
element.put("uploader", element.getString("uploaderName"))
val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
if (v == null || v.thumb.isEmpty()) continue
items.add(v)
}
return items
}
@ -440,51 +434,43 @@ class InfoUtil(private val context: Context) {
}
private fun getFormatsFromYTDL(url: String) : List<Format> {
try {
val request = YoutubeDLRequest(url)
request.addOption("--print", "%(formats)s")
request.addOption("--print", "%(duration)s")
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
val request = YoutubeDLRequest(url)
request.addOption("--print", "%(formats)s")
request.addOption("--print", "%(duration)s")
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
val useHeader = sharedPreferences.getBoolean("use_header", false)
val header = sharedPreferences.getString("useragent_header", "")
if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()) {
request.addOption("--proxy", proxy)
}
val res = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = try {
val lineSeparator = System.getProperty("line.separator")
res.out.split(lineSeparator!!).toTypedArray()
} catch (e: Exception) {
arrayOf(res.out)
}
val json = results[0]
val jsonArray = JSONArray(json)
return parseYTDLFormats(jsonArray)
} catch (e: Exception) {
e.printStackTrace()
Looper.prepare().run {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
val useHeader = sharedPreferences.getBoolean("use_header", false)
val header = sharedPreferences.getString("useragent_header", "")
if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
}
}
return emptyList()
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()) {
request.addOption("--proxy", proxy)
}
val res = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = try {
val lineSeparator = System.getProperty("line.separator")
res.out.split(lineSeparator!!).toTypedArray()
} catch (e: Exception) {
arrayOf(res.out)
}
val json = results[0]
val jsonArray = JSONArray(json)
return parseYTDLFormats(jsonArray)
}
fun getFormatsMultiple(urls: List<String>, progress: (progress: List<Format>) -> Unit){
@ -561,147 +547,139 @@ class InfoUtil(private val context: Context) {
fun getFromYTDL(query: String): ArrayList<ResultItem?> {
val items = arrayListOf<ResultItem?>()
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
try {
val request : YoutubeDLRequest
if (query.contains("http")){
request = YoutubeDLRequest(query)
val request : YoutubeDLRequest
if (query.contains("http")){
request = YoutubeDLRequest(query)
}else{
request = YoutubeDLRequest(emptyList())
if (searchEngine == "ytsearchmusic"){
request.addOption("--default-search", "https://music.youtube.com/search?q=")
request.addOption("ytsearch25:\"${query}\"")
}else{
request = YoutubeDLRequest(emptyList())
if (searchEngine == "ytsearchmusic"){
request.addOption("--default-search", "https://music.youtube.com/search?q=")
request.addOption("ytsearch25:\"${query}\"")
}else{
request.addOption("${searchEngine}25:\"${query}\"")
}
request.addOption("${searchEngine}25:\"${query}\"")
}
}
request.addOption("--flat-playlist")
request.addOption("-j")
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
request.addOption("--flat-playlist")
request.addOption("-j")
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
val useHeader = sharedPreferences.getBoolean("use_header", false)
val header = sharedPreferences.getString("useragent_header", "")
if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
}
val useHeader = sharedPreferences.getBoolean("use_header", false)
val header = sharedPreferences.getString("useragent_header", "")
if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
}
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val results: List<String?> = try {
val lineSeparator = System.getProperty("line.separator")
youtubeDLResponse.out.split(lineSeparator!!)
} catch (e: Exception) {
listOf(youtubeDLResponse.out)
}.filter { it.isNotBlank() }
for (result in results) {
if (result.isNullOrBlank()) continue
val jsonObject = JSONObject(result)
val title = if (jsonObject.has("title")) {
if (jsonObject.getString("title") == "[Private video]") continue
jsonObject.getString("title")
} else {
jsonObject.getString("webpage_url_basename")
}
var author: String = if (jsonObject.has("uploader")) jsonObject.getString("uploader") else ""
if (author.isEmpty() || author == "null"){
author = if (jsonObject.has("channel")) jsonObject.getString("channel") else ""
if (author.isEmpty() || author == "null"){
author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else ""
}
}
var duration = ""
runCatching {
if (jsonObject.has("duration")) {
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
}
}
var thumb: String? = ""
if (jsonObject.has("thumbnail")) {
thumb = jsonObject.getString("thumbnail")
} else if (jsonObject.has("thumbnails")) {
val thumbs = jsonObject.getJSONArray("thumbnails")
if (thumbs.length() > 0){
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
}
}
val website = jsonObject.getString(listOf("ie_key", "extractor_key", "extractor").first { jsonObject.has(it) })
var playlistTitle: String? = ""
if (jsonObject.has("playlist_title")) playlistTitle = jsonObject.getString("playlist_title")
if(playlistTitle.equals(query)) playlistTitle = ""
if (playlistTitle?.isNotBlank() == true){
playlistTitle += "[${jsonObject.getString("playlist_index")}]"
}
val formatsInJSON = if (jsonObject.has("formats") && jsonObject.get("formats") is JSONArray) jsonObject.getJSONArray("formats") else null
val formats : ArrayList<Format> = parseYTDLFormats(formatsInJSON)
val chaptersInJSON = if (jsonObject.has("chapters") && jsonObject.get("chapters") is JSONArray) jsonObject.getJSONArray("chapters") else null
val listType: Type = object : TypeToken<List<ChapterItem>>() {}.type
var chapters : ArrayList<ChapterItem> = arrayListOf()
if (chaptersInJSON != null){
chapters = Gson().fromJson(chaptersInJSON.toString(), listType)
}
var urls = "";
if(jsonObject.has("requested_formats")) {
val requestedFormats = jsonObject.getJSONArray("requested_formats")
val urlList = mutableListOf<String>()
val length = requestedFormats.length()-1
for (i in length downTo 0) {
urlList.add(requestedFormats.getJSONObject(i).getString("url"))
}
urls = urlList.joinToString("\n")
}
val url = if (jsonObject.has("url") && results.size > 1){
jsonObject.getString("url")
}else{
if (Patterns.WEB_URL.matcher(query).matches()){
query
}else{
jsonObject.getString("webpage_url")
}
}
items.add(ResultItem(0,
url,
title,
author,
duration,
thumb!!,
website,
playlistTitle!!,
formats,
urls,
chapters
)
)
}
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val results: List<String?> = try {
val lineSeparator = System.getProperty("line.separator")
youtubeDLResponse.out.split(lineSeparator!!)
} catch (e: Exception) {
val intent = Intent(context, ErrorDialogActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.putExtra("title", context.resources.getString(R.string.no_results))
intent.putExtra("message", e.message)
context.startActivity(intent)
e.printStackTrace()
listOf(youtubeDLResponse.out)
}.filter { it.isNotBlank() }
for (result in results) {
if (result.isNullOrBlank()) continue
val jsonObject = JSONObject(result)
val title = if (jsonObject.has("title")) {
if (jsonObject.getString("title") == "[Private video]") continue
jsonObject.getString("title")
} else {
jsonObject.getString("webpage_url_basename")
}
var author: String = if (jsonObject.has("uploader")) jsonObject.getString("uploader") else ""
if (author.isEmpty() || author == "null"){
author = if (jsonObject.has("channel")) jsonObject.getString("channel") else ""
if (author.isEmpty() || author == "null"){
author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else ""
}
}
var duration = ""
runCatching {
if (jsonObject.has("duration")) {
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
}
}
var thumb: String? = ""
if (jsonObject.has("thumbnail")) {
thumb = jsonObject.getString("thumbnail")
} else if (jsonObject.has("thumbnails")) {
val thumbs = jsonObject.getJSONArray("thumbnails")
if (thumbs.length() > 0){
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
}
}
val website = jsonObject.getString(listOf("ie_key", "extractor_key", "extractor").first { jsonObject.has(it) })
var playlistTitle: String? = ""
if (jsonObject.has("playlist_title")) playlistTitle = jsonObject.getString("playlist_title")
if(playlistTitle.equals(query)) playlistTitle = ""
if (playlistTitle?.isNotBlank() == true){
playlistTitle += "[${jsonObject.getString("playlist_index")}]"
}
val formatsInJSON = if (jsonObject.has("formats") && jsonObject.get("formats") is JSONArray) jsonObject.getJSONArray("formats") else null
val formats : ArrayList<Format> = parseYTDLFormats(formatsInJSON)
val chaptersInJSON = if (jsonObject.has("chapters") && jsonObject.get("chapters") is JSONArray) jsonObject.getJSONArray("chapters") else null
val listType: Type = object : TypeToken<List<ChapterItem>>() {}.type
var chapters : ArrayList<ChapterItem> = arrayListOf()
if (chaptersInJSON != null){
chapters = Gson().fromJson(chaptersInJSON.toString(), listType)
}
var urls = "";
if(jsonObject.has("requested_formats")) {
val requestedFormats = jsonObject.getJSONArray("requested_formats")
val urlList = mutableListOf<String>()
val length = requestedFormats.length()-1
for (i in length downTo 0) {
urlList.add(requestedFormats.getJSONObject(i).getString("url"))
}
urls = urlList.joinToString("\n")
}
val url = if (jsonObject.has("url") && results.size > 1){
jsonObject.getString("url")
}else{
if (Patterns.WEB_URL.matcher(query).matches()){
query
}else{
jsonObject.getString("webpage_url")
}
}
items.add(ResultItem(0,
url,
title,
author,
duration,
thumb!!,
website,
playlistTitle!!,
formats,
urls,
chapters
)
)
}
return items
}
@ -748,89 +726,81 @@ class InfoUtil(private val context: Context) {
}
fun getMissingInfo(url: String): ResultItem? {
try {
val request = YoutubeDLRequest(url)
request.addOption("--flat-playlist")
request.addOption("-J")
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
val request = YoutubeDLRequest(url)
request.addOption("--flat-playlist")
request.addOption("-J")
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
val useHeader = sharedPreferences.getBoolean("use_header", false)
val header = sharedPreferences.getString("useragent_header", "")
if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
val useHeader = sharedPreferences.getBoolean("use_header", false)
val header = sharedPreferences.getString("useragent_header", "")
if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
}
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val jsonObject = JSONObject(youtubeDLResponse.out)
var author: String = if (jsonObject.has("uploader")) jsonObject.getString("uploader") else ""
if (author.isEmpty() || author == "null"){
author = if (jsonObject.has("channel")) jsonObject.getString("channel") else ""
if (author.isEmpty() || author == "null"){
author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else ""
}
}
var duration = ""
runCatching {
if (jsonObject.has("duration")) {
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
}
}
var thumb: String? = ""
if (jsonObject.has("thumbnail")) {
thumb = jsonObject.getString("thumbnail")
} else if (jsonObject.has("thumbnails")) {
val thumbs = jsonObject.getJSONArray("thumbnails")
if (thumbs.length() > 0){
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
}
}
val isPlaylist = jsonObject.has("playlist_count")
return ResultItem(
0,
url,
if (isPlaylist){
"[${jsonObject.getInt("playlist_count")} Items] ${jsonObject.getString("title")}"
}else{
jsonObject.getString("title")
},
author,
duration,
thumb!!,
jsonObject.getString("extractor"),
if (isPlaylist) jsonObject.getString("title") else "",
arrayListOf(),
"",
arrayListOf(),
System.currentTimeMillis()
)
} catch (e: Exception) {
Looper.prepare().run {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
e.printStackTrace()
}
return null
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val jsonObject = JSONObject(youtubeDLResponse.out)
var author: String = if (jsonObject.has("uploader")) jsonObject.getString("uploader") else ""
if (author.isEmpty() || author == "null"){
author = if (jsonObject.has("channel")) jsonObject.getString("channel") else ""
if (author.isEmpty() || author == "null"){
author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else ""
}
}
var duration = ""
runCatching {
if (jsonObject.has("duration")) {
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
}
}
var thumb: String? = ""
if (jsonObject.has("thumbnail")) {
thumb = jsonObject.getString("thumbnail")
} else if (jsonObject.has("thumbnails")) {
val thumbs = jsonObject.getJSONArray("thumbnails")
if (thumbs.length() > 0){
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
}
}
val isPlaylist = jsonObject.has("playlist_count")
return ResultItem(
0,
url,
if (isPlaylist){
""
}else{
jsonObject.getString("title")
},
author,
duration,
thumb!!,
jsonObject.getString("extractor"),
if (isPlaylist) jsonObject.getString("title") else "",
arrayListOf(),
"",
arrayListOf(),
System.currentTimeMillis()
)
}
@ -983,9 +953,9 @@ class InfoUtil(private val context: Context) {
val downDir : File
if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){
downDir = File(FileUtil.formatPath(downloadItem.downloadPath))
request.addOption("--no-quiet")
request.addOption("-no-simulate")
request.addOption("--print", "after_video:'%(filename)s'")
request.addOption("--progress")
request.addOption("-v")
}else{
val cacheDir = FileUtil.getCachePath(context)
downDir = File(cacheDir, downloadItem.id.toString())

View file

@ -2,16 +2,18 @@ package com.deniscerri.ytdlnis.util
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.net.Uri
import android.os.Build
import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import android.text.format.DateFormat
@ -34,22 +36,32 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.allViews
import androidx.core.view.children
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.App
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.ui.downloadcard.ConfigureDownloadBottomSheetDialog
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.ui.downloadcard.VideoCutListener
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
@ -61,6 +73,7 @@ import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.DateValidatorPointForward
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.textfield.TextInputLayout
import com.google.android.material.timepicker.MaterialTimePicker
@ -68,7 +81,9 @@ import com.google.android.material.timepicker.TimeFormat
import com.neo.highlight.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import me.zhanghai.android.fastscroll.FastScrollerBuilder
@ -416,17 +431,17 @@ object UiUtil {
author!!.text = item.author.ifEmpty { "`${context.getString(R.string.defaultValue)}`" }
// BUTTON ----------------------------------
val btn = bottomSheet.findViewById<MaterialButton>(R.id.downloads_download_button_type)
val btn = bottomSheet.findViewById<FloatingActionButton>(R.id.download_button_type)
when (item.type) {
DownloadViewModel.Type.audio -> {
btn!!.icon = ContextCompat.getDrawable(context, R.drawable.ic_music)
btn?.setImageResource(R.drawable.ic_music)
}
DownloadViewModel.Type.video -> {
btn!!.icon = ContextCompat.getDrawable(context, R.drawable.ic_video)
btn?.setImageResource(R.drawable.ic_video)
}
else -> {
btn!!.icon = ContextCompat.getDrawable(context, R.drawable.ic_terminal)
btn?.setImageResource(R.drawable.ic_terminal)
}
}
@ -542,6 +557,135 @@ object UiUtil {
)
}
fun showHistoryItemDetailsCard(
item: HistoryItem?,
context: Activity,
isPresent: Boolean,
redownloadItem: (HistoryItem) -> Unit,
redownloadShowDownloadCard: (HistoryItem) -> Unit,
){
val bottomSheet = BottomSheetDialog(context)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet)
val title = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)
title!!.text = item!!.title
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
author!!.text = item.author
// BUTTON ----------------------------------
val btn = bottomSheet.findViewById<FloatingActionButton>(R.id.download_button_type)
if (item.type == DownloadViewModel.Type.audio) {
if (isPresent) {
btn?.setImageResource(R.drawable.ic_music_downloaded)
} else {
btn?.setImageResource(R.drawable.ic_music)
}
} else if (item.type == DownloadViewModel.Type.video) {
if (isPresent) {
btn?.setImageResource(R.drawable.ic_video_downloaded)
} else {
btn?.setImageResource(R.drawable.ic_video)
}
}else{
btn?.setImageResource(R.drawable.ic_terminal)
}
if (isPresent){
btn?.setOnClickListener {
shareFileIntent(context, listOf(item.downloadPath))
}
}
val time = bottomSheet.findViewById<TextView>(R.id.time)
val formatNote = bottomSheet.findViewById<TextView>(R.id.format_note)
val container = bottomSheet.findViewById<TextView>(R.id.container_chip)
val codec = bottomSheet.findViewById<TextView>(R.id.codec)
val fileSize = bottomSheet.findViewById<TextView>(R.id.file_size)
val calendar = Calendar.getInstance()
calendar.timeInMillis = item.time * 1000L
time!!.text = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(calendar.time)
time.isClickable = false
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
View.GONE
else formatNote!!.text = item.format.format_note
if (item.format.container != "") container!!.text = item.format.container.uppercase()
else container!!.visibility = View.GONE
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
}
val file = File(item.downloadPath)
val fileSizeReadable = FileUtil.convertFileSize(if (file.exists()) file.length() else item.format.filesize)
if (fileSizeReadable == "?") fileSize!!.visibility = View.GONE
else fileSize!!.text = fileSizeReadable
val link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link)
val url = item.url
link!!.text = url
link.tag = item.id
link.setOnClickListener{
openLinkIntent(context, item.url, bottomSheet)
}
link.setOnLongClickListener{
copyLinkToClipBoard(context, item.url, bottomSheet)
true
}
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = item.id
remove.setOnClickListener{
showRemoveHistoryItemDialog(item, context, delete = { item, deleteFile ->
})
bottomSheet.dismiss()
}
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
openFile!!.tag = item.id
openFile.setOnClickListener{
openFileIntent(context, item.downloadPath)
}
val redownload = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
redownload!!.tag = item.id
redownload.setOnClickListener{
redownloadItem(item)
bottomSheet.cancel()
}
redownload.setOnLongClickListener {
redownloadShowDownloadCard(item)
bottomSheet.cancel()
true
}
if (!isPresent) openFile.visibility = View.GONE
else redownload.visibility = View.GONE
bottomSheet.show()
val displayMetrics = DisplayMetrics()
context.windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
fun showFormatDetails(format: Format, activity: Activity){
val bottomSheet = BottomSheetDialog(activity)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
@ -653,16 +797,16 @@ object UiUtil {
builder.setView(inputLayout)
builder.setPositiveButton(
context.getString(R.string.ok)
) { dialog: DialogInterface?, which: Int ->
) { _: DialogInterface?, _: Int ->
ok(editText.text.toString())
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(R.string.cancel)
) { dialog: DialogInterface?, which: Int -> }
) { _: DialogInterface?, _: Int -> }
builder.setNeutralButton("?") { dialog: DialogInterface?, which: Int ->
builder.setNeutralButton("?") { _: DialogInterface?, _: Int ->
val browserIntent =
Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/yt-dlp/yt-dlp#subtitle-options"))
context.startActivity(browserIntent)
@ -670,7 +814,7 @@ object UiUtil {
val dialog = builder.create()
editText.doOnTextChanged { text, start, before, count ->
editText.doOnTextChanged { _, _, _, _ ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
}
dialog.show()
@ -923,35 +1067,14 @@ object UiUtil {
val filenameTemplate = view.findViewById<Chip>(R.id.filename_template)
filenameTemplate.setOnClickListener {
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(context.getString(R.string.file_name_template))
val inputLayout = context.layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = context.getString(R.string.file_name_template)
if (items.size == 1 || items.all { it.customFileNameTemplate == items[0].customFileNameTemplate }){
editText.setText(items[0].customFileNameTemplate)
val currentFilename = if (items.size == 1 || items.all { it.customFileNameTemplate == items[0].customFileNameTemplate }){
items[0].customFileNameTemplate
}else {
""
}
editText.setSelection(editText.text.length)
builder.setView(inputLayout)
builder.setPositiveButton(
context.getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
filenameTemplateSet(editText.text.toString())
showFilenameTemplateDialog(context, currentFilename) {
filenameTemplateSet(it)
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
dialog.show()
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
editText!!.postDelayed({
editText.requestFocus()
imm.showSoftInput(editText, 0)
}, 300)
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
@ -1022,35 +1145,14 @@ object UiUtil {
val filenameTemplate = view.findViewById<Chip>(R.id.filename_template)
filenameTemplate.setOnClickListener {
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(context.getString(R.string.file_name_template))
val inputLayout = context.layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = context.getString(R.string.file_name_template)
if (items.size == 1 || items.all { it.customFileNameTemplate == items[0].customFileNameTemplate }){
editText.setText(items[0].customFileNameTemplate)
val currentFilename = if (items.size == 1 || items.all { it.customFileNameTemplate == items[0].customFileNameTemplate }){
items[0].customFileNameTemplate
}else {
""
}
editText.setSelection(editText.text.length)
builder.setView(inputLayout)
builder.setPositiveButton(
context.getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
filenameTemplateSet(editText.text.toString())
showFilenameTemplateDialog(context, currentFilename) {
filenameTemplateSet(it)
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
dialog.show()
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
editText!!.postDelayed({
editText.requestFocus()
imm.showSoftInput(editText, 0)
}, 300)
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
val sponsorBlock = view.findViewById<Chip>(R.id.sponsorblock_filters)
@ -1222,9 +1324,6 @@ object UiUtil {
}
}
fun EditText.setTextAndRecalculateWidth(t : String){
val scale = context.resources.displayMetrics.density
this.setText(t)
@ -1239,4 +1338,258 @@ object UiUtil {
}
this.requestLayout()
}
fun handleResultResponse(context: Activity, it: ResultViewModel.ResultsUiState){
val title = context.getString(it.errorMessage!!.first)
val message = it.errorMessage!!.second
val errDialog = MaterialAlertDialogBuilder(context)
.setTitle(title)
.setMessage(message)
for (a in it.actions!!){
when(a.second){
ResultViewModel.ResultAction.COPY_LOG -> {
errDialog.setPositiveButton(a.first) { d:DialogInterface?, _:Int ->
val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setText(message)
d?.dismiss()
}
}
}
}
errDialog.show()
}
@SuppressLint("SetTextI18n")
fun handleDownloadsResponse(context: MainActivity, it: DownloadViewModel.DownloadsUiState, downloadViewModel: DownloadViewModel){
val downloadAnywayAction = it.actions?.first { it.second == DownloadViewModel.DownloadsAction.DOWNLOAD_ANYWAY}
if (downloadAnywayAction != null){
if (downloadAnywayAction.third == null) return
val downloads =downloadAnywayAction.third!!.toMutableList()
val ids = downloadAnywayAction.third!!.map { it.id }
val titles = downloadAnywayAction.third!!.map { it.title }
val title = context.getString(it.errorMessage!!.first)
val message = context.getString(it.errorMessage!!.second)
val errDialog = MaterialAlertDialogBuilder(context)
.setTitle(title)
.setMessage(message)
var dialog: Dialog? = null
val linearLayout = context.layoutInflater.inflate(R.layout.already_exists_card, null) as LinearLayout
ids.forEachIndexed { index, id ->
val alreadyExistsItem = context.layoutInflater.inflate(R.layout.already_exists_item, null)
alreadyExistsItem.tag = id.toString()
val ttle = alreadyExistsItem.findViewById<Button>(R.id.already_exists_title)
ttle.text = "${index + 1}. ${titles[index]}"
CoroutineScope(Dispatchers.IO).launch {
val editBtn = alreadyExistsItem.findViewById<Button>(R.id.already_exists_edit)
var downloadItem: DownloadItem = downloadViewModel.getItemByID(id)
val historyItem: HistoryItem? = downloadViewModel.getHistoryItemById(id)
if (historyItem != null){
val idx = downloads.indexOfFirst { it.id == historyItem.id }
downloadItem = downloadViewModel.createDownloadItemFromHistory(historyItem)
downloadItem.id = historyItem.id
downloads[idx] = downloadItem
}
withContext(Dispatchers.Main){
editBtn.visibility = View.VISIBLE
}
editBtn.setOnClickListener {
val resultItem = downloadViewModel.createResultItemFromDownload(downloadItem)
val onItemUpdated = object: ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
override fun onDownloadItemUpdate(
resultItemID: Long,
item: DownloadItem
) {
context.lifecycleScope.launch {
val idx = downloads.indexOfFirst { it.id == item.id }
downloads[idx] = item
withContext(Dispatchers.Main){
ttle.text = "${index + 1}. ${item.title}"
}
}
}
}
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem, onItemUpdated)
bottomSheet.show(context.supportFragmentManager, "configureDownloadSingleSheet")
}
if (historyItem != null){
ttle.setOnClickListener {
showHistoryItemDetailsCard(historyItem, context, isPresent = true,
redownloadItem = { },
redownloadShowDownloadCard = {
val sheet = DownloadBottomSheetDialog(
result = downloadViewModel.createResultItemFromHistory(it),
type = it.type
)
sheet.show(context.supportFragmentManager, "downloadSingleSheet")
}
)
}
}
}
ttle.setOnLongClickListener {
showGenericDeleteDialog(context, ttle.text.toString(), accepted = {
linearLayout.removeView(alreadyExistsItem)
if (linearLayout.childCount == 0){
dialog?.dismiss()
}
})
true
}
linearLayout.addView(alreadyExistsItem)
}
errDialog.setView(linearLayout)
errDialog.setPositiveButton(downloadAnywayAction.first) { d:DialogInterface?, _:Int ->
CoroutineScope(Dispatchers.IO).launch {
linearLayout.allViews.forEach {view ->
val downloadItem = downloads.first { it.id == (view.tag as String).toLong()}
downloadItem.id = 0
downloadViewModel.queueDownloads(listOf(downloadItem), true)
}
}
d?.dismiss()
}
errDialog.setNegativeButton(R.string.schedule) { d:DialogInterface?, _:Int ->
showDatePicker(context.supportFragmentManager) { calendar ->
CoroutineScope(Dispatchers.IO).launch {
val items = mutableListOf<DownloadItem>()
linearLayout.children.forEach {view ->
val downloadItem = downloads.first { it.id == (view.tag as String).toLong()}
downloadItem.downloadStartTime = calendar.timeInMillis
downloadItem.id = 0
items.add(downloadItem)
}
runBlocking {
val chunks = items.chunked(10)
for (c in chunks) {
downloadViewModel.queueDownloads(c, true)
}
val first = items.first()
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(first.downloadStartTime)
withContext(Dispatchers.Main){
Toast.makeText(context, context.getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
}
}
withContext(Dispatchers.Main){
d?.dismiss()
}
}
}
}
dialog = errDialog.show()
}
}
fun showGenericDeleteDialog(context: Context, itemTitle: String, accepted: () -> Unit){
val deleteDialog = MaterialAlertDialogBuilder(context)
deleteDialog.setTitle(context.getString(R.string.you_are_going_to_delete) + " \"" + itemTitle + "\"!")
deleteDialog.setNegativeButton(context.getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(context.getString(R.string.ok)) { _: DialogInterface?, _: Int ->
accepted()
}
deleteDialog.show()
}
fun showRemoveHistoryItemDialog(item: HistoryItem, context: Activity, delete: (item: HistoryItem, deleteFile: Boolean) -> Unit){
val deleteFile = booleanArrayOf(false)
val deleteDialog = MaterialAlertDialogBuilder(context)
deleteDialog.setTitle(context.getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
val path = item.downloadPath
val file = File(path)
if (file.exists() && path.isNotEmpty()) {
deleteDialog.setMultiChoiceItems(
arrayOf(context.getString(R.string.delete_file_too)),
booleanArrayOf(false)
) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b }
}
deleteDialog.setNegativeButton(context.getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(context.getString(R.string.ok)) { _: DialogInterface?, _: Int ->
delete(item, deleteFile[0])
}
deleteDialog.show()
}
fun showFilenameTemplateDialog(context: Activity, currentFilename: String, dialogTitle: String = context.getString(R.string.file_name_template), filenameSelected: (f: String) -> Unit){
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(dialogTitle)
val view = context.layoutInflater.inflate(R.layout.filename_template_dialog, null)
val editText = view.findViewById<EditText>(R.id.filename_edittext)
view.findViewById<TextInputLayout>(R.id.filename).hint = context.getString(R.string.file_name_template)
editText.setText(currentFilename)
editText.setSelection(editText.text.length)
builder.setView(view)
builder.setPositiveButton(
context.getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
filenameSelected(editText.text.toString())
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
view.findViewById<View>(R.id.suggested).visibility = View.GONE
val dialog = builder.create()
dialog.show()
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
editText!!.postDelayed({
editText.requestFocus()
imm.showSoftInput(editText, 0)
}, 300)
//handle suggestion chips
CoroutineScope(Dispatchers.IO).launch {
val chipGroup = view.findViewById<ChipGroup>(R.id.filename_suggested_chipgroup)
val chips = mutableListOf<Chip>()
context.getStringArray(R.array.filename_templates).forEachIndexed { index, s ->
val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
tmp.text = s.split("___")[0]
tmp.id = index
if (Build.VERSION.SDK_INT >= 26){
tmp.tooltipText = s.split("___")[1]
}
tmp.setOnClickListener {
val c = it as Chip
if(!c.isChecked){
editText.setText(editText.text.toString().replace(c.text.toString(), ""))
editText.setSelection(editText.text.length)
}else{
editText.append(c.text)
}
}
chips.add(tmp)
}
withContext(Dispatchers.Main){
view.findViewById<View>(R.id.suggested).visibility = View.VISIBLE
chips.forEach {
it.isChecked = editText.text.contains(it.text)
chipGroup!!.addView(it)
}
}
}
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
}

View file

@ -41,6 +41,7 @@ class UpdateUtil(var context: Context) {
Toast.LENGTH_LONG
).show()
}
val skippedVersions = sharedPreferences.getString("skip_updates", "")?.split(",")?.distinct()?.toMutableList() ?: mutableListOf()
val res = getGithubReleases()
if (res.isEmpty()){
@ -67,6 +68,8 @@ class UpdateUtil(var context: Context) {
isInLatest = false
}
if (skippedVersions.contains(v.tag_name)) isInLatest = true
if (isInLatest){
result(context.getString(R.string.you_are_in_latest_version))
return
@ -77,6 +80,11 @@ class UpdateUtil(var context: Context) {
.setTitle(v.tag_name)
.setMessage(v.body)
.setIcon(R.drawable.ic_update_app)
.setNeutralButton(R.string.skip){ d: DialogInterface?, _:Int ->
skippedVersions.add(v.tag_name)
sharedPreferences.edit().putString("skip_updates", skippedVersions.joinToString(",")).apply()
d?.dismiss()
}
.setNegativeButton(context.resources.getString(R.string.cancel)) { _: DialogInterface?, _: Int -> }
.setPositiveButton(context.resources.getString(R.string.update)) { _: DialogInterface?, _: Int ->
runCatching {

View file

@ -136,13 +136,10 @@ class DownloadWorker(
)
if (logDownloads){
runBlocking {
logItem.id = logRepo.insert(logItem)
downloadItem.logID = logItem.id
dao.update(downloadItem)
}
runBlocking {
logItem.id = logRepo.insert(logItem)
downloadItem.logID = logItem.id
dao.update(downloadItem)
}
val noCache = !sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()
@ -156,10 +153,8 @@ class DownloadWorker(
line, progress.toInt(), 0, title,
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
)
if (logDownloads){
CoroutineScope(Dispatchers.IO).launch {
logRepo.update(line, logItem.id)
}
CoroutineScope(Dispatchers.IO).launch {
logRepo.update(line, logItem.id)
}
}
}.onSuccess {
@ -169,7 +164,7 @@ class DownloadWorker(
if (noCache){
setProgressAsync(workDataOf("progress" to 100, "output" to "Scanning Files", "id" to downloadItem.id))
val p = it.out.split("\r")
val p = it.out.split("\n")
.filter { it.startsWith("'/storage") }
.map { it.removePrefix("'") }
.map { it.removeSuffix("'\n") }
@ -241,6 +236,8 @@ class DownloadWorker(
if (logDownloads){
logRepo.update(it.out, logItem.id)
}else{
logRepo.delete(logItem)
}
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/existing_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical">
</LinearLayout>

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/playlist_card_constraintLayout"
android:layout_width="match_parent"
android:layout_marginBottom="10dp"
android:checkable="true"
android:paddingHorizontal="20dp"
android:paddingVertical="5dp"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:layout_height="wrap_content">
<Button
android:id="@+id/already_exists_title"
style="@style/Widget.Material3.Button.TonalButton.Icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:autoLink="all"
android:gravity="start"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginEnd="10dp"
app:layout_constraintEnd_toStartOf="@+id/already_exists_edit"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="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_height="wrap_content"
android:visibility="gone"
app:icon="@drawable/ic_edit"
app:layout_constraintBottom_toBottomOf="@id/already_exists_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/already_exists_title" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:text="@string/sort_by"
android:textSize="12sp"
android:layout_margin="20dp"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/date"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:drawablePadding="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:text="@string/date_added"
app:drawableStartCompat="@drawable/ic_arrow_up" />
<TextView
android:id="@+id/title"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:drawablePadding="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:text="@string/title"
app:drawableStartCompat="@drawable/ic_arrow_up" />
<TextView
android:id="@+id/length"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:drawablePadding="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:text="@string/length"
app:drawableStartCompat="@drawable/ic_arrow_up" />
</LinearLayout>
</FrameLayout>

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:padding="10dp"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/filename"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/filename_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/suggested"
android:layout_width="0dp"
android:padding="10dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/filename"
android:text="@string/suggested" />
<ScrollView
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHeight_max="200dp"
android:paddingHorizontal="10dp"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/suggested">
<com.google.android.material.chip.ChipGroup
android:id="@+id/filename_suggested_chipgroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chipSpacingVertical="-7dp"
app:chipSpacingHorizontal="5dp"
app:selectionRequired="false"
app:singleSelection="false">
</com.google.android.material.chip.ChipGroup>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -34,6 +34,22 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/format_loading_progress"
android:visibility="gone"
android:layout_width="0dp"
android:indeterminate="true"
app:trackColor="#000"
android:layout_gravity="bottom"
android:alpha="0.3"
android:scaleY="200"
app:layout_constraintTop_toTopOf="@id/container"
app:layout_constraintBottom_toBottomOf="@id/container"
app:layout_constraintStart_toStartOf="@id/container"
app:layout_constraintEnd_toEndOf="@id/container"
android:layout_height="wrap_content" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -92,6 +92,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:paddingVertical="5dp"
android:text="@string/video" />
<LinearLayout
@ -111,6 +112,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:paddingVertical="5dp"
android:text="@string/audio" />
<LinearLayout

View file

@ -22,66 +22,74 @@
app:navigationIcon="@drawable/ic_back"
android:layout_height="match_parent"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_scrollFlags="scroll|enterAlways"
android:layout_margin="10dp">
</com.google.android.material.appbar.AppBarLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<HorizontalScrollView
android:id="@+id/chips_command"
android:layout_width="wrap_content"
android:scrollbars="none"
android:layout_margin="10dp"
android:layout_height="wrap_content">
<LinearLayout
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
<LinearLayout
android:id="@+id/history_selection_chips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:selectionRequired="false"
app:singleSelection="false">
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/newTemplate"
android:id="@+id/sortChip"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/new_template"
app:chipIcon="@drawable/ic_plus" />
android:text="@string/sort_by"
app:chipIcon="@drawable/ic_down" />
<com.google.android.material.chip.Chip
android:id="@+id/shortcuts"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/shortcuts"
app:chipIcon="@drawable/ic_shortcut" />
<View style="@style/Divider.Vertical"/>
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.chip.ChipGroup
android:layout_width="match_parent"
app:selectionRequired="false"
app:singleSelection="true"
android:layout_height="wrap_content">
</LinearLayout>
<com.google.android.material.chip.Chip
android:id="@+id/newTemplate"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/new_template"
app:chipIcon="@drawable/ic_plus" />
</HorizontalScrollView>
<com.google.android.material.chip.Chip
android:id="@+id/shortcuts"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/shortcuts"
app:chipIcon="@drawable/ic_shortcut" />
<androidx.recyclerview.widget.RecyclerView
android:layout_below="@+id/chips_command"
android:id="@+id/template_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
>
</com.google.android.material.chip.ChipGroup>
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
</RelativeLayout>
</HorizontalScrollView>
</RelativeLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/template_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
>
</androidx.recyclerview.widget.RecyclerView>
<include layout="@layout/history_no_results"

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout style="@style/Widget.Material3.BottomSheet"
<FrameLayout xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
@ -17,10 +18,11 @@
android:padding="20dp">
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintEnd_toStartOf="@+id/downloads_download_button_layout"
app:layout_constraintEnd_toStartOf="@+id/download_button_type"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
@ -43,28 +45,19 @@
</LinearLayout>
<LinearLayout
android:id="@+id/downloads_download_button_layout"
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/download_button_type"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:clickable="false"
android:elevation="0dp"
app:borderWidth="0dp"
app:cornerRadius="10dp"
app:elevation="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/downloads_download_button_type"
style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Secondary"
android:layout_width="55dp"
android:layout_height="55dp"
android:layout_margin="10dp"
android:layout_marginBottom="10dp"
android:clickable="false"
android:elevation="0dp"
app:borderWidth="0dp"
app:cornerRadius="10dp"
app:elevation="0dp" />
</LinearLayout>
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/share" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,59 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/webview_appbarlayout"
app:liftOnScroll="true"
android:background="@android:color/transparent"
android:elevation="0dp"
app:elevation="0dp"
android:fitsSystemWindows="true"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/webviewToolbar"
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/webview_appbarlayout"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll"
app:navigationIcon="?attr/homeAsUpIndicator"
>
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:elevation="0dp"
android:fitsSystemWindows="true"
app:elevation="0dp"
app:liftOnScroll="true">
<Button
android:id="@+id/generate"
android:layout_gravity="end"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/ok"
app:icon="@drawable/ic_check"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="PrivateResource" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/webviewToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll"
app:navigationIcon="?attr/homeAsUpIndicator">
</com.google.android.material.appbar.MaterialToolbar>
<Button
android:id="@+id/generate"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginHorizontal="10dp"
android:autoLink="all"
android:text="@string/ok"
app:icon="@drawable/ic_check"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="PrivateResource" />
</com.google.android.material.appbar.AppBarLayout>
</com.google.android.material.appbar.MaterialToolbar>
<RelativeLayout
android:id="@+id/rl"
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/webview_compose"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:layout_height="match_parent">
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/coordinatorLayout2"
tools:composableName="com.deniscerri.ytdlnis.ui.more.WebViewActivity.WebViewView" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/webview_compose"
tools:composableName="com.deniscerri.ytdlnis.ui.more.WebViewActivity.WebViewView"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,6 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/search_command"
android:title="@string/search"
android:icon="@drawable/ic_search"
app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="androidx.appcompat.widget.SearchView"/>
<item
android:id="@+id/import_clipboard"
android:title="@string/import_from_clipboard"

View file

@ -100,13 +100,7 @@
android:id="@+id/resultCardDetailsDialog"
android:name="com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog"
android:label="ResultCardDetailsDialog" />
<navigation android:id="@+id/settingsNav"
app:startDestination="@id/settingsFrame">
<fragment
android:id="@+id/settingsFrame"
android:name="com.deniscerri.ytdlnis.ui.more.settings.SettingsFragmentFrame"
android:label="settings_frame"
tools:layout="@layout/fragment_settings" />
<navigation android:id="@+id/settingsNav">
<fragment
android:id="@+id/folderSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.FolderSettingsFragment"

View file

@ -821,4 +821,111 @@
<item>m4a|mp4a|aac</item>
<item>opus</item>
</string-array>
<string-array name="filename_templates">
<item>%(uploader)s___(string): Full name of the video uploader)</item>
<item>%(title)s___(string): Video title)</item>
<item>%(id)s___(string): Video identifier)</item>
<item>%(fulltitle)s___(string): Video title ignoring live timestamp and generic title)</item>
<item>%(ext)s___(string): Video filename extension)</item>
<item>%(alt_title)s___(string): A secondary title of the video)</item>
<item>%(description)s___(string): The description of the video)</item>
<item>%(display_id)s___(string): An alternative identifier for the video)</item>
<item>%(license)s___(string): License name the video is licensed under)</item>
<item>%(creator)s___(string): The creator of the video)</item>
<item>%(timestamp)s___(numeric): UNIX timestamp of the moment the video became available)</item>
<item>%(upload_date)s___(string): Video upload date in UTC (YYYYMMDD))</item>
<item>%(release_timestamp)s___(numeric): UNIX timestamp of the moment the video was released)</item>
<item>%(release_date)s___(string): The date (YYYYMMDD) when the video was released in UTC)</item>
<item>%(modified_timestamp)s___(numeric): UNIX timestamp of the moment the video was last modified)</item>
<item>%(modified_date)s___(string): The date (YYYYMMDD) when the video was last modified in UTC)</item>
<item>%(uploader_id)s___(string): Nickname or id of the video uploader)</item>
<item>%(channel)s___(string): Full name of the channel the video is uploaded on)</item>
<item>%(channel_id)s___(string): Id of the channel)</item>
<item>%(channel_follower_count)s___(numeric): Number of followers of the channel)</item>
<item>%(channel_is_verified)s___(boolean): Whether the channel is verified on the platform)</item>
<item>%(location)s___(string): Physical location where the video was filmed)</item>
<item>%(duration)s___(numeric): Length of the video in seconds)</item>
<item>%(duration_string)s___(string): Length of the video (HH:mm:ss))</item>
<item>%(view_count)s___(numeric): How many users have watched the video on the platform)</item>
<item>%(concurrent_view_count)s___(numeric): How many users are currently watching the video on the platform.)</item>
<item>%(like_count)s___(numeric): Number of positive ratings of the video)</item>
<item>%(dislike_count)s___(numeric): Number of negative ratings of the video)</item>
<item>%(repost_count)s___(numeric): Number of reposts of the video)</item>
<item>%(average_rating)s___(numeric): Average rating give by users, the scale used depends on the webpage)</item>
<item>%(comment_count)s___(numeric): Number of comments on the video (For some extractors, comments are only downloaded at the end, and so this field cannot be used))</item>
<item>%(age_limit)s___(numeric): Age restriction for the video (years))</item>
<item>%(live_status)s___(string): One of "not_live", "is_live", "is_upcoming", "was_live", "post_live" (was live, but VOD is not yet processed))</item>
<item>%(is_live)s___(boolean): Whether this video is a live stream or a fixed-length video)</item>
<item>%(was_live)s___(boolean): Whether this video was originally a live stream)</item>
<item>%(playable_in_embed)s___(string): Whether this video is allowed to play in embedded players on other sites)</item>
<item>%(availability)s___(string): Whether the video is "private", "premium_only", "subscriber_only", "needs_auth", "unlisted" or "public")</item>
<item>%(start_time)s___(numeric): Time in seconds where the reproduction should start, as specified in the URL)</item>
<item>%(end_time)s___(numeric): Time in seconds where the reproduction should end, as specified in the URL)</item>
<item>%(extractor)s___(string): Name of the extractor)</item>
<item>%(extractor_key)s___(string): Key name of the extractor)</item>
<item>%(epoch)s___(numeric): Unix epoch of when the information extraction was completed)</item>
<item>%(autonumber)s___(numeric): Number that will be increased with each download, starting at --autonumber-start, padded with leading zeros to 5 digits)</item>
<item>%(video_autonumber)s___(numeric): Number that will be increased with each video)</item>
<item>%(n_entries)s___(numeric): Total number of extracted items in the playlist)</item>
<item>%(playlist_id)s___(string): Identifier of the playlist that contains the video)</item>
<item>%(playlist_title)s___(string): Name of the playlist that contains the video)</item>
<item>%(playlist)s___(string): playlist_id or playlist_title)</item>
<item>%(playlist_count)s___(numeric): Total number of items in the playlist. May not be known if entire playlist is not extracted)</item>
<item>%(playlist_index)s___(numeric): Index of the video in the playlist padded with leading zeros according the final index)</item>
<item>%(playlist_autonumber)s___(numeric): Position of the video in the playlist download queue padded with leading zeros according to the total length of the playlist)</item>
<item>%(playlist_uploader)s___(string): Full name of the playlist uploader)</item>
<item>%(playlist_uploader_id)s___(string): Nickname or id of the playlist uploader)</item>
<item>%(webpage_url)s___(string): A URL to the video webpage which if given to yt-dlp should allow to get the same result again)</item>
<item>%(webpage_url_basename)s___(string): The basename of the webpage URL)</item>
<item>%(webpage_url_domain)s___(string): The domain of the webpage URL)</item>
<item>%(original_url)s___(string): The URL given by the user (or same as webpage_url for playlist entries))</item>
<item>%(chapter)s___(string): Name or title of the chapter the video belongs to)</item>
<item>%(chapter_number)s___(numeric): Number of the chapter the video belongs to)</item>
<item>%(chapter_id)s___(string): Id of the chapter the video belongs to)</item>
<item>%(series)s___(string): Title of the series or programme the video episode belongs to)</item>
<item>%(season)s___(string): Title of the season the video episode belongs to)</item>
<item>%(season_number)s___(numeric): Number of the season the video episode belongs to)</item>
<item>%(season_id)s___(string): Id of the season the video episode belongs to)</item>
<item>%(episode)s___(string): Title of the video episode)</item>
<item>%(episode_number)s___(numeric): Number of the video episode within a season)</item>
<item>%(episode_id)s___(string): Id of the video episode)</item>
<item>%(track)s___(string): Title of the track)</item>
<item>%(track_number)s___(numeric): Number of the track within an album or a disc)</item>
<item>%(track_id)s___(string): Id of the track)</item>
<item>%(artist)s___(string): Artist(s) of the track)</item>
<item>%(genre)s___(string): Genre(s) of the track)</item>
<item>%(album)s___(string): Title of the album the track belongs to)</item>
<item>%(album_type)s___(string): Type of the album)</item>
<item>%(album_artist)s___(string): List of all artists appeared on the album)</item>
<item>%(disc_number)s___(numeric): Number of the disc or other physical medium the track belongs to)</item>
<item>%(release_year)s___(numeric): Year (YYYY) when the album was released)</item>
<item>%(section_title)s___(string): Title of the chapter)</item>
<item>%(section_number)s___(numeric): Number of the chapter within the file)</item>
<item>%(section_start)s___(numeric): Start time of the chapter in seconds)</item>
<item>%(section_end)s___(numeric): End time of the chapter in seconds)</item>
</string-array>
<string-array name="modify_download_card">
<item>@string/schedule</item>
<item>@string/title</item>
<item>@string/author</item>
<item>@string/container</item>
<item>@string/adjust_audio</item>
<item>@string/adjust_video</item>
<item>@string/adjust_templates</item>
<item>@string/url</item>
</string-array>
<string-array name="modify_download_card_values">
<item>schedule</item>
<item>title</item>
<item>author</item>
<item>container</item>
<item>adjust_audio</item>
<item>adjust_video</item>
<item>adjust_templates</item>
<item>url</item>
</string-array>
</resources>

View file

@ -30,6 +30,7 @@
<string name="confirm_delete_history_desc">Delete the entire history, permanently</string>
<string name="confirm_delete_history">Confirm</string>
<string name="search_history_hint">Search from History</string>
<string name="search_command_hint">Search from Commands</string>
<string name="no_results">No Results</string>
<string name="more">More</string>
<string name="update_app">Check for updates</string>
@ -310,4 +311,11 @@
<string name="remember_download_type">Remember Download Type</string>
<string name="remember_download_type_summary">Select the last opened download type for the current item</string>
<string name="user_agent_header">User-Agent header</string>
<string name="length">Length</string>
<string name="security">Security</string>
<string name="license">License</string>
<string name="download_already_exists_summary">Do you still want to download?</string>
<string name="schedule">Schedule</string>
<string name="skip">Skip</string>
<string name="modify_download_card">Modify Download Card</string>
</resources>

View file

@ -37,6 +37,13 @@
app:title="@string/download_over_metered_networks" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_cookie_24"
android:key="use_cookies"
app:title="@string/use_cookies" />
<EditTextPreference
android:icon="@drawable/baseline_network_locked_24"
app:key="proxy"
@ -111,10 +118,14 @@
<EditTextPreference
app:key="retries"
android:defaultValue=""
app:useSimpleSummaryProvider="true"
app:title="@string/retries" />
<EditTextPreference
app:key="fragment_retries"
android:defaultValue=""
app:useSimpleSummaryProvider="true"
app:title="@string/fragment_retries" />
</PreferenceCategory>

View file

@ -3,16 +3,19 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
android:defaultValue=""
app:icon="@drawable/ic_music_downloaded"
app:key="music_path"
app:title="@string/music_directory" />
<Preference
android:defaultValue=""
app:icon="@drawable/ic_video_downloaded"
app:key="video_path"
app:title="@string/video_directory" />
<Preference
android:defaultValue=""
app:icon="@drawable/ic_terminal"
app:key="command_path"
app:title="@string/command_directory" />
@ -43,14 +46,14 @@
<EditTextPreference
<Preference
android:icon="@drawable/ic_textformat"
app:key="file_name_template"
app:useSimpleSummaryProvider="true"
app:defaultValue="%(uploader)s - %(title)s"
app:title="@string/file_name_template" />
<EditTextPreference
<Preference
android:icon="@drawable/ic_textformat"
app:key="file_name_template_audio"
app:useSimpleSummaryProvider="true"

View file

@ -46,6 +46,15 @@
app:key="hide_thumbnails"
app:title="@string/hide_thumbnails" />
<MultiSelectListPreference
app:icon="@drawable/ic_card"
android:dialogTitle="@string/modify_download_card"
android:entries="@array/modify_download_card"
android:entryValues="@array/modify_download_card_values"
android:defaultValue="@array/modify_download_card_values"
app:key="modify_download_card"
app:title="@string/modify_download_card" />
</PreferenceCategory>

View file

@ -149,7 +149,7 @@
app:title="@string/video_format" />
<ListPreference
android:defaultValue=""
android:defaultValue="avc|h264"
android:entries="@array/video_codec"
android:entryValues="@array/video_codec_values"
android:icon="@drawable/ic_code"
@ -157,7 +157,7 @@
app:title="@string/preferred_video_codec" />
<ListPreference
android:defaultValue=""
android:defaultValue="m4a|mp4a|aac"
android:entries="@array/audio_codec"
android:entryValues="@array/audio_codec_values"
android:icon="@drawable/ic_code"
@ -211,9 +211,4 @@
android:summary="@string/use_extra_commands_summary"
app:title="@string/use_extra_commands" />
<Preference
app:isPreferenceVisible="false"
android:key="use_cookies"
android:defaultValue="false" />
</PreferenceScreen>

View file

@ -73,7 +73,7 @@
app:icon="@drawable/baseline_security_24"
app:key="security"
app:summary="🔐 YTDLnis - Privacy Policy"
app:title="SECURITY">
app:title="@string/security">
<intent
android:action="android.intent.action.VIEW"
android:data="https://github.com/deniscerri/ytdlnis/blob/main/SECURITY.md" />
@ -83,7 +83,7 @@
app:icon="@drawable/baseline_draw_24"
app:key="license"
app:summary="GNU General Public License v3.0"
app:title="LICENSE">
app:title="@string/license">
<intent
android:action="android.intent.action.VIEW"
android:data="https://github.com/deniscerri/ytdlnis/blob/main/LICENSE" />

View file

@ -69,6 +69,11 @@
app:summary="@string/update_app_summary"
app:title="@string/update_app" />
<EditTextPreference
app:isPreferenceVisible="false"
app:key="skip_updates"
app:defaultValue="" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"

View file

@ -21,7 +21,7 @@ buildscript {
coroutineVer = '1.7.3'
retrofitVer = "2.9.0"
kodeinVer = "7.16.0"
navVer = "2.6.0"
navVer = "2.7.4"
media3_version = "1.1.1"
agp_version = '8.1.0'
agp_version1 = '7.4.2'
@ -34,11 +34,11 @@ buildscript {
}
plugins {
id 'com.android.application' version '7.4.2' apply false
id 'com.android.library' version '7.4.2' apply false
id 'com.android.application' version '8.1.1' apply false
id 'com.android.library' version '8.1.1' apply false
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
id "org.jetbrains.kotlin.plugin.serialization" version "1.8.10" apply false
id 'com.android.test' version '7.4.2' apply false
id 'com.android.test' version '8.1.1' apply false
id 'com.google.devtools.ksp' version '1.8.10-1.0.9' apply false
}

View file

@ -1,6 +1,6 @@
#Sat Apr 08 15:57:50 CEST 2023
#Wed Oct 18 18:36:39 CEST 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists