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

View file

@ -76,4 +76,17 @@
-dontwarn com.google.android.exoplayer2.** -dontwarn com.google.android.exoplayer2.**
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. # 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 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.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@ -212,16 +213,6 @@
</intent-filter> </intent-filter>
</activity> </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 <service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false" 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.ui.more.settings.SettingsActivity
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.ThemeUtil import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UpdateUtil import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder

View file

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

View file

@ -2,11 +2,31 @@ package com.deniscerri.ytdlnis.database.dao
import androidx.room.* import androidx.room.*
import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.TemplateShortcut import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface CommandTemplateDao { 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") @Query("SELECT * FROM commandTemplates ORDER BY id DESC")
fun getAllTemplates() : List<CommandTemplate> fun getAllTemplates() : List<CommandTemplate>

View file

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

View file

@ -1,8 +1,10 @@
package com.deniscerri.ytdlnis.database.repository package com.deniscerri.ytdlnis.database.repository
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.TemplateShortcut import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -10,10 +12,22 @@ class CommandTemplateRepository(private val commandDao: CommandTemplateDao) {
val items : Flow<List<CommandTemplate>> = commandDao.getAllTemplatesFlow() val items : Flow<List<CommandTemplate>> = commandDao.getAllTemplatesFlow()
val shortcuts : Flow<List<TemplateShortcut>> = commandDao.getAllShortcutsFlow() val shortcuts : Flow<List<TemplateShortcut>> = commandDao.getAllShortcutsFlow()
enum class CommandTemplateSortType {
DATE, TITLE, LENGTH
}
fun getAll() : List<CommandTemplate> { fun getAll() : List<CommandTemplate> {
return commandDao.getAllTemplates() 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 { fun getTotalNumber() : Int {
return commandDao.getTotalNumber() return commandDao.getTotalNumber()
} }

View file

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

View file

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

View file

@ -5,13 +5,17 @@ import android.content.ClipboardManager
import android.content.Context.CLIPBOARD_SERVICE import android.content.Context.CLIPBOARD_SERVICE
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.CommandTemplateExport 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.models.TemplateShortcut
import com.deniscerri.ytdlnis.database.repository.CommandTemplateRepository import com.deniscerri.ytdlnis.database.repository.CommandTemplateRepository
import com.deniscerri.ytdlnis.database.repository.HistoryRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -21,15 +25,55 @@ import kotlinx.serialization.json.Json
class CommandTemplateViewModel(private val application: Application) : AndroidViewModel(application) { class CommandTemplateViewModel(private val application: Application) : AndroidViewModel(application) {
private val repository: CommandTemplateRepository 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>> val shortcuts : LiveData<List<TemplateShortcut>>
private val jsonFormat = Json { prettyPrint = true } private val jsonFormat = Json { prettyPrint = true }
init { init {
val dao = DBManager.getInstance(application).commandTemplateDao val dao = DBManager.getInstance(application).commandTemplateDao
repository = CommandTemplateRepository(dao) repository = CommandTemplateRepository(dao)
items = repository.items.asLiveData() allItems = repository.items.asLiveData()
shortcuts = repository.shortcuts.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 { fun getTemplate(itemId: Long): CommandTemplate {

View file

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

View file

@ -10,7 +10,7 @@ import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.repository.HistoryRepository 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.database.repository.HistoryRepository.HistorySortType
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -18,7 +18,7 @@ import kotlinx.coroutines.launch
class HistoryViewModel(application: Application) : AndroidViewModel(application) { class HistoryViewModel(application: Application) : AndroidViewModel(application) {
private val repository : HistoryRepository private val repository : HistoryRepository
val sortOrder = MutableLiveData(HistorySort.DESC) val sortOrder = MutableLiveData(SORTING.DESC)
val sortType = MutableLiveData(HistorySortType.DATE) val sortType = MutableLiveData(HistorySortType.DATE)
val websiteFilter = MutableLiveData("") val websiteFilter = MutableLiveData("")
private val queryFilter = MutableLiveData("") private val queryFilter = MutableLiveData("")
@ -55,11 +55,11 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
fun setSorting(sort: HistorySortType){ fun setSorting(sort: HistorySortType){
if (sortType.value != sort){ if (sortType.value != sort){
sortOrder.value = HistorySort.DESC sortOrder.value = SORTING.DESC
}else{ }else{
sortOrder.value = if (sortOrder.value == HistorySort.DESC) { sortOrder.value = if (sortOrder.value == SORTING.DESC) {
HistorySort.ASC SORTING.ASC
} else HistorySort.DESC } else SORTING.DESC
} }
sortType.value = sort sortType.value = sort
} }
@ -76,7 +76,7 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
formatFilter.value = filter 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)) _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){ fun delete(item: HistoryItem, deleteFile: Boolean) = viewModelScope.launch(Dispatchers.IO){
repository.delete(item) repository.delete(item, deleteFile)
if (deleteFile){
FileUtil.deleteFile(item.downloadPath)
}
} }
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { 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.models.SearchHistoryItem
import com.deniscerri.ytdlnis.database.repository.ResultRepository import com.deniscerri.ytdlnis.database.repository.ResultRepository
import com.deniscerri.ytdlnis.database.repository.SearchHistoryRepository import com.deniscerri.ytdlnis.database.repository.SearchHistoryRepository
import com.deniscerri.ytdlnis.util.UiUtil
import kotlinx.coroutines.Dispatchers 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.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.regex.Pattern import java.util.regex.Pattern
@ -27,19 +31,28 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
val repository : ResultRepository val repository : ResultRepository
private val searchHistoryRepository : SearchHistoryRepository private val searchHistoryRepository : SearchHistoryRepository
val items : LiveData<List<ResultItem>> val items : LiveData<List<ResultItem>>
val loadingItems = MutableLiveData<Boolean>() data class ResultsUiState(
private val sharedPreferences: SharedPreferences var processing: Boolean,
var state: ResultsState = ResultsState.IDLE var errorMessage: Pair<Int, String>?,
enum class ResultsState { var actions: MutableList<Pair<Int, ResultAction>>?
PROCESSING, IDLE )
enum class ResultAction {
COPY_LOG
} }
val uiState: MutableStateFlow<ResultsUiState> = MutableStateFlow(ResultsUiState(
processing = false,
errorMessage = null,
actions = null
))
private val sharedPreferences: SharedPreferences
init { init {
val dao = DBManager.getInstance(application).resultDao val dao = DBManager.getInstance(application).resultDao
repository = ResultRepository(dao, getApplication<Application>().applicationContext) repository = ResultRepository(dao, getApplication<Application>().applicationContext)
searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao) searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao)
items = repository.allResults.asLiveData() items = repository.allResults.asLiveData()
loadingItems.postValue(false)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
} }
@ -64,54 +77,38 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
deleteAll() deleteAll()
} }
} }
suspend fun parseQueries(inputQueries: List<String>) : MutableList<ResultItem?>{ suspend fun parseQueries(inputQueries: List<String>) : List<ResultItem?> {
state = ResultsState.PROCESSING if (inputQueries.size > 1){
return if (inputQueries.size == 1){
parseQuery(inputQueries[0], true)
}else {
repository.itemCount.value = inputQueries.size 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?> { uiState.update {it.copy(processing = true, errorMessage = null, actions = null)}
if (resetResults)
loadingItems.postValue(true)
val type = getQueryType(inputQuery)
var res = arrayListOf<ResultItem?>()
return withContext(Dispatchers.IO){ return withContext(Dispatchers.IO){
try { val res = mutableListOf<ResultItem?>()
when (type) { inputQueries.forEach { inputQuery ->
"Search" -> { val type = getQueryType(inputQuery)
res = repository.search(inputQuery, resetResults) try {
} when (type) {
"YT_Video" -> { "Search" -> res.addAll(repository.search(inputQuery, resetResults))
res = repository.getOne(inputQuery, resetResults) "YT_Video" -> res.addAll(repository.getYoutubeVideo(inputQuery, resetResults))
} "YT_Playlist" -> res.addAll(repository.getPlaylist(inputQuery, resetResults))
"YT_Playlist" -> { else -> res.addAll(repository.getDefault(inputQuery, resetResults))
res = repository.getPlaylist(inputQuery, resetResults)
}
"Default" -> {
res = 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 res
} }
} }
private fun getQueryType(inputQuery: String) : String { private fun getQueryType(inputQuery: String) : String {
var type = "Search" var type = "Search"
val p = Pattern.compile("(^(https?)://(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)") 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.provider.Settings
import android.util.Log import android.util.Log
import android.util.Patterns import android.util.Patterns
import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import android.widget.TextView
import android.widget.Toast
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat 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.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.ui.downloadcard.SelectPlaylistItemsDialog import com.deniscerri.ytdlnis.ui.downloadcard.SelectPlaylistItemsDialog
import com.deniscerri.ytdlnis.util.ThemeUtil import com.deniscerri.ytdlnis.util.ThemeUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch 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.ui.downloadcard.ResultCardDetailsDialog
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.ThemeUtil import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.facebook.shimmer.ShimmerFrameLayout import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.appbar.AppBarLayout 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 com.google.android.material.search.SearchView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.* import java.util.*
@ -105,6 +108,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
activity = getActivity() activity = getActivity()
mainActivity = activity as MainActivity? mainActivity = activity as MainActivity?
quickLaunchSheet = false quickLaunchSheet = false
infoUtil = InfoUtil(requireContext())
return fragmentView return fragmentView
} }
@ -136,6 +140,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
searchSuggestionsLinearLayout = view.findViewById(R.id.search_suggestions_linear_layout) searchSuggestionsLinearLayout = view.findViewById(R.id.search_suggestions_linear_layout)
searchHistory = view.findViewById(R.id.search_history_scroll_view) searchHistory = view.findViewById(R.id.search_history_scroll_view)
searchHistoryLinearLayout = view.findViewById(R.id.search_history_linear_layout) 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()) } runCatching { materialToolbar!!.title = ThemeUtil.getStyledAppName(requireContext()) }
@ -174,30 +181,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
quickLaunchSheet = true 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() 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) val downloadSelectedFab = downloadFabs!!.findViewById<ExtendedFloatingActionButton>(R.id.download_selected_fab)
downloadSelectedFab.tag = "downloadSelected" downloadSelectedFab.tag = "downloadSelected"
downloadSelectedFab.setOnClickListener(this) 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() { override fun onResume() {
super.onResume() super.onResume()
if(arguments?.getString("url") == null){ if(arguments?.getString("url") == null){
if (resultViewModel.state == ResultViewModel.ResultsState.IDLE){ if (!resultViewModel.uiState.value.processing){
resultViewModel.checkTrending() resultViewModel.checkTrending()
}else{
resultViewModel.loadingItems.postValue(true)
} }
}else{ }else{
arguments?.remove("url") arguments?.remove("url")
@ -302,7 +311,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
} }
infoUtil = InfoUtil(requireContext())
searchView!!.addTransitionListener { _, _, newState -> searchView!!.addTransitionListener { _, _, newState ->
if (newState == SearchView.TransitionState.SHOWN) { if (newState == SearchView.TransitionState.SHOWN) {
val currentProvider = sharedPreferences?.getString("search_engine", "ytsearch") 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.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UiUtil.setTextAndRecalculateWidth import com.deniscerri.ytdlnis.util.UiUtil.setTextAndRecalculateWidth
import com.deniscerri.ytdlnis.util.VideoPlayerUtil import com.deniscerri.ytdlnis.util.VideoPlayerUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -496,10 +497,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
} }
chip.setOnLongClickListener { chip.setOnLongClickListener {
val deleteDialog = MaterialAlertDialogBuilder(requireContext()) UiUtil.showGenericDeleteDialog(requireContext(), chip.text.toString(), accepted = {
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 ->
player.seekTo(0) player.seekTo(0)
player.pause() player.pause()
chipGroup.removeView(chip) chipGroup.removeView(chip)
@ -509,8 +507,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
player.stop() player.stop()
dismiss() dismiss()
} }
} })
deleteDialog.show()
true true
} }

View file

@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.ui.downloadcard
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
@ -11,11 +12,13 @@ import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isEmpty import androidx.core.view.isEmpty
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -52,6 +55,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
lateinit var downloadItem : DownloadItem lateinit var downloadItem : DownloadItem
lateinit var title : TextInputLayout lateinit var title : TextInputLayout
lateinit var author : TextInputLayout lateinit var author : TextInputLayout
lateinit var preferences: SharedPreferences
lateinit var shownFields: List<String>
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -63,6 +68,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
infoUtil = InfoUtil(requireContext()) infoUtil = InfoUtil(requireContext())
genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources) genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources)
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
shownFields = preferences.getStringSet("modify_download_card", setOf())!!.toList()
return fragmentView return fragmentView
} }
@ -82,6 +89,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
try { try {
title = view.findViewById(R.id.title_textinput) title = view.findViewById(R.id.title_textinput)
title.visibility = if (shownFields.contains("title")) View.VISIBLE else View.GONE
if (title.editText?.text?.isEmpty() == true){ if (title.editText?.text?.isEmpty() == true){
title.editText!!.setText(downloadItem.title) 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 = view.findViewById(R.id.author_textinput)
author.visibility = if (shownFields.contains("author")) View.VISIBLE else View.GONE
if (author.editText?.text?.isEmpty() == true){ if (author.editText?.text?.isEmpty() == true){
author.editText!!.setText(downloadItem.author) author.editText!!.setText(downloadItem.author)
} }
@ -158,9 +167,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
if (currentDownloadItem!!.type != Type.audio){ if (currentDownloadItem!!.type != Type.audio){
downloadItem.type = Type.audio downloadItem.type = Type.audio
runCatching { runCatching {
downloadItem.format = downloadItem.format = downloadViewModel.getFormat(downloadItem.allFormats, Type.audio)
downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
.maxByOrNull { it.filesize }!!
}.onFailure { }.onFailure {
downloadItem.format = genericAudioFormats.last() downloadItem.format = genericAudioFormats.last()
} }
@ -172,6 +179,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
var containerPreference = sharedPreferences.getString("audio_format", "Default") var containerPreference = sharedPreferences.getString("audio_format", "Default")
if (containerPreference == "Default") containerPreference = getString(R.string.defaultValue) if (containerPreference == "Default") containerPreference = getString(R.string.defaultValue)
val container = view.findViewById<TextInputLayout>(R.id.downloadContainer) val container = view.findViewById<TextInputLayout>(R.id.downloadContainer)
container.visibility = if (shownFields.contains("container")) View.VISIBLE else View.GONE
val containerAutoCompleteTextView = val containerAutoCompleteTextView =
view.findViewById<AutoCompleteTextView>(R.id.container_textview) 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) }) resultItem?.formats?.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) })
if (resultItem != null){ if (resultItem != null){
resultViewModel.update(resultItem!!) 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 = "" 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) view.findViewById<LinearLayout>(R.id.adjust).apply {
bottomSheetDialog.show(parentFragmentManager, "extraCommands") 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){ }catch (e : Exception){
e.printStackTrace() e.printStackTrace()
} }

View file

@ -1,11 +1,8 @@
package com.deniscerri.ytdlnis.ui.downloadcard package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
@ -20,14 +17,12 @@ import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.core.content.edit import androidx.core.content.edit
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao 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.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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 com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -205,6 +201,11 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
viewPager2.setPageTransformer(BackgroundToForegroundPageTransformer()) viewPager2.setPageTransformer(BackgroundToForegroundPageTransformer())
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button) 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) 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) 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) val updateItem = view.findViewById<Button>(R.id.update_item)
if (Patterns.WEB_URL.matcher(result.url).matches()){ if (Patterns.WEB_URL.matcher(result.url).matches()){
link.text = result.url 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 //update in the background if there is no data
if(result.title.isEmpty() && currentDownloadItem == null && !sharedPreferences.getBoolean("quick_download", false)){ if(result.title.isEmpty() && currentDownloadItem == null && !sharedPreferences.getBoolean("quick_download", false)){
initUpdateData(view) 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{ private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem{
@ -342,7 +365,7 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
return@launch return@launch
} }
val result = resultViewModel.parseQuery(result.url, true) val result = resultViewModel.parseQueries(listOf(result.url))
if (result.isEmpty()){ if (result.isEmpty()){
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
Looper.prepare().run { Looper.prepare().run {
@ -376,6 +399,12 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
shimmerLoadingSubtitle.stopShimmer() 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{ }else{
//open multi download card instead //open multi download card instead
if (activity is ShareActivity){ 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) { override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog) super.onCancel(dialog)
cleanUp() cleanUp()
@ -407,7 +478,7 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
private fun cleanUp(){ private fun cleanUp(){
kotlin.runCatching { kotlin.runCatching {
repeat(parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.backStackEntryCount ?: 0){ repeat((parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.backStackEntryCount?.minus(1))?: 0){
parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.popBackStack() parentFragmentManager.findFragmentByTag("downloadSingleSheet")?.fragmentManager?.popBackStack()
} }
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("downloadSingleSheet")!!).commit() parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("downloadSingleSheet")!!).commit()

View file

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

View file

@ -32,6 +32,7 @@ import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem 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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -506,6 +506,18 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
true 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>){ private fun updateFileSize(items: List<Long>){

View file

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

View file

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

View file

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

View file

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

View file

@ -19,7 +19,6 @@ import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
@ -41,28 +40,22 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.HistoryAdapter import com.deniscerri.ytdlnis.adapter.HistoryAdapter
import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.repository.HistoryRepository 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.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHistoryBinding import com.deniscerri.ytdlnis.databinding.FragmentHistoryBinding
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog 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
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetDialog 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.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.runBlocking 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. * A fragment representing a list of Items.
@ -163,8 +156,8 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyViewModel.sortOrder.observe(viewLifecycleOwner){ historyViewModel.sortOrder.observe(viewLifecycleOwner){
if (it != null){ if (it != null){
when(it){ when(it){
HistorySort.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down) SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down)
HistorySort.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up) 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){ when(order){
HistorySort.DESC ->{ SORTING.DESC ->{
item.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_up, 0,0,0) item.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_up, 0,0,0)
} }
HistorySort.ASC -> { SORTING.ASC -> {
item.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_down, 0,0,0) 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 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) { 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 item = findItem(itemID)
val title = bottomSheet!!.findViewById<TextView>(R.id.bottom_sheet_title) UiUtil.showHistoryItemDetailsCard(item, requireActivity(), isPresent,
title!!.text = item!!.title redownloadItem = {
val author = bottomSheet!!.findViewById<TextView>(R.id.bottom_sheet_author) val downloadItem = downloadViewModel.createDownloadItemFromHistory(it)
author!!.text = item.author runBlocking{
downloadViewModel.queueDownloads(listOf(downloadItem))
// BUTTON ---------------------------------- }
val btn = bottomSheet!!.findViewById<MaterialButton>(R.id.downloads_download_button_type) historyViewModel.delete(it, false)
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))
} }
} ) {
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( val sheet = DownloadBottomSheetDialog(
result = downloadViewModel.createResultItemFromHistory(item), result = downloadViewModel.createResultItemFromHistory(it),
type = item.type) type = it.type
)
sheet.show(parentFragmentManager, "downloadSingleSheet") 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) { override fun onCardSelect(itemID: Long, isChecked: Boolean) {
@ -666,7 +536,10 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
ItemTouchHelper.LEFT -> { ItemTouchHelper.LEFT -> {
val deletedItem = historyList!![position] val deletedItem = historyList!![position]
historyAdapter!!.notifyItemChanged(position) historyAdapter!!.notifyItemChanged(position)
removeItem(deletedItem!!) UiUtil.showRemoveHistoryItemDialog(deletedItem!!, requireActivity(),
delete = { item, deleteFile ->
historyViewModel.delete(item, deleteFile)
})
} }
ItemTouchHelper.RIGHT -> { ItemTouchHelper.RIGHT -> {
val item = historyList!![position]!! val item = historyList!![position]!!

View file

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

View file

@ -4,11 +4,19 @@ import android.content.DialogInterface
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Bundle 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.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window
import android.widget.RelativeLayout 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.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -21,9 +29,12 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.TemplatesAdapter import com.deniscerri.ytdlnis.adapter.TemplatesAdapter
import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel 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.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar 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 templatesList: List<CommandTemplate>
private lateinit var noResults: RelativeLayout private lateinit var noResults: RelativeLayout
private lateinit var mainActivity: MainActivity private lateinit var mainActivity: MainActivity
private lateinit var sortChip: Chip
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -58,6 +70,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
topAppBar = view.findViewById(R.id.logs_toolbar) topAppBar = view.findViewById(R.id.logs_toolbar)
topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() } topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() }
noResults = view.findViewById(R.id.no_results) noResults = view.findViewById(R.id.no_results)
sortChip = view.findViewById(R.id.sortChip)
templatesAdapter = templatesAdapter =
TemplatesAdapter( TemplatesAdapter(
@ -75,35 +88,90 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java] commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
commandTemplateViewModel.items.observe(viewLifecycleOwner) { commandTemplateViewModel.allItems.observe(viewLifecycleOwner) {
if (it.isEmpty()) noResults.visibility = View.VISIBLE if (it.isEmpty()) noResults.visibility = View.VISIBLE
else noResults.visibility = View.GONE 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() initMenu()
initChips() initChips()
} }
private fun initMenu() { private fun initMenu() {
topAppBar.setOnMenuItemClickListener { m: MenuItem -> val onActionExpandListener: MenuItem.OnActionExpandListener =
val itemId = m.itemId object : MenuItem.OnActionExpandListener {
if (itemId == R.id.export_clipboard) { override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean {
lifecycleScope.launch{ return true
withContext(Dispatchers.IO){
commandTemplateViewModel.exportToClipboard()
}
Snackbar.make(recyclerView, getString(R.string.copied_to_clipboard), Snackbar.LENGTH_LONG).show()
} }
}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 true
@ -111,6 +179,45 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
} }
private fun initChips() { 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) val new = view?.findViewById<Chip>(R.id.newTemplate)
new?.setOnClickListener { new?.setOnClickListener {
UiUtil.showCommandTemplateCreationOrUpdatingSheet(null,mainActivity, this, commandTemplateViewModel) {} UiUtil.showCommandTemplateCreationOrUpdatingSheet(null,mainActivity, this, commandTemplateViewModel) {}
@ -121,8 +228,25 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
} }
} }
companion object { private fun changeSortIcon(item: TextView, order: SORTING){
private const val TAG = "DownloadLogActivity" 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) { override fun onItemClick(commandTemplate: CommandTemplate, index: Int) {

View file

@ -62,15 +62,6 @@ class MoreFragment : Fragment() {
terminateApp = view.findViewById(R.id.terminate) terminateApp = view.findViewById(R.id.terminate)
settings = view.findViewById(R.id.settings) 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 { terminal.setOnClickListener {
val intent = Intent(context, TerminalActivity::class.java) val intent = Intent(context, TerminalActivity::class.java)
startActivity(intent) startActivity(intent)

View file

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

View file

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

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.more.settings package com.deniscerri.ytdlnis.ui.more.settings
import android.app.Activity
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -8,6 +9,7 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
@ -32,6 +34,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
private var highContrast: SwitchPreferenceCompat? = null private var highContrast: SwitchPreferenceCompat? = null
private var locale: ListPreference? = null private var locale: ListPreference? = null
private var showTerminalShareIcon: SwitchPreferenceCompat? = null private var showTerminalShareIcon: SwitchPreferenceCompat? = null
private var ignoreBatteryOptimization: Preference? = null
private var updateUtil: UpdateUtil? = null private var updateUtil: UpdateUtil? = null
private var activeDownloadCount = 0 private var activeDownloadCount = 0
@ -127,7 +130,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
true true
} }
var ignoreBatteryOptimization = findPreference<Preference>("ignore_battery") ignoreBatteryOptimization = findPreference<Preference>("ignore_battery")
val packageName: String = requireContext().packageName val packageName: String = requireContext().packageName
val pm = requireContext().applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager val pm = requireContext().applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
if (pm.isIgnoringBatteryOptimizations(packageName)) { if (pm.isIgnoringBatteryOptimizations(packageName)) {
@ -140,8 +143,16 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
val intent = Intent() val intent = Intent()
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:" + requireContext().packageName) intent.data = Uri.parse("package:" + requireContext().packageName)
startActivity(intent) ignoreBatteryLauncher.launch(intent)
true 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.Bundle
import android.os.Environment import android.os.Environment
import android.util.LayoutDirection import android.util.LayoutDirection
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit import androidx.core.content.edit
@ -171,7 +172,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
Environment.getExternalStoragePublicDirectory( Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS), "YTDLnis_${BuildConfig.VERSION_NAME}_${currentTime.get( Environment.DIRECTORY_DOWNLOADS), "YTDLnis_${BuildConfig.VERSION_NAME}_${currentTime.get(
Calendar.YEAR)}-${currentTime.get(Calendar.MONTH) + 1}-${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.delete()
saveFile.createNewFile() saveFile.createNewFile()
saveFile.writeText(Gson().toJson(json)) saveFile.writeText(Gson().toJson(json))
@ -223,15 +224,14 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
private fun backupSettings(preferences: SharedPreferences) : JsonArray { private fun backupSettings(preferences: SharedPreferences) : JsonArray {
runCatching { runCatching {
val prefs = preferences.all val prefs = preferences.all
prefs.remove("app_language")
val arr = JsonArray() val arr = JsonArray()
prefs.forEach { prefs.forEach {
if (it.key != "app_language"){ val obj = JsonObject()
val obj = JsonObject() obj.addProperty("key", it.key)
obj.addProperty("key", it.key) obj.addProperty("value", it.value.toString())
obj.addProperty("value", it.value.toString()) obj.addProperty("type", it.value!!::class.simpleName)
obj.addProperty("type", it.value!!::class.simpleName) arr.add(obj)
arr.add(obj)
}
} }
return arr return arr
} }
@ -398,27 +398,26 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit(commit = true){ PreferenceManager.getDefaultSharedPreferences(requireContext()).edit(commit = true){
clear() clear()
val prefs = json.getAsJsonArray("settings") val prefs = json.getAsJsonArray("settings")
val preferencesKeys = preferences.all.map { it.key }
prefs.forEach { prefs.forEach {
val key : String = it.asJsonObject.get("key").toString().replace("\"", "") val key : String = it.asJsonObject.get("key").toString().replace("\"", "")
if (preferencesKeys.contains(key)){ when(it.asJsonObject.get("type").toString().replace("\"", "")){
when(it.asJsonObject.get("type").toString().replace("\"", "")){ "String" -> {
"String" -> { val value = it.asJsonObject.get("value").toString().replace("\"", "")
val value = it.asJsonObject.get("value").toString().replace("\"", "") putString(key, value)
putString(key, value) }
} "Boolean" -> {
"Boolean" -> { val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean() Log.e("REST", value.toString())
putBoolean(key, value) Log.e("REST", key)
} putBoolean(key, value)
"Int" -> { }
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt() "Int" -> {
putInt(key, value) val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
} putInt(key, value)
"HashSet" -> { }
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",") "HashSet" -> {
putStringSet(key, value.toHashSet()) 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 package com.deniscerri.ytdlnis.util
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources import android.content.res.Resources
import android.os.Looper 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.Format
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.ErrorDialogActivity
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
@ -387,17 +385,13 @@ class InfoUtil(private val context: Context) {
val items = arrayListOf<ResultItem?>() val items = arrayListOf<ResultItem?>()
val url = "$pipedURL/trending?region=$countryCODE" val url = "$pipedURL/trending?region=$countryCODE"
val res = genericArrayRequest(url) val res = genericArrayRequest(url)
try { for (i in 0 until res.length()) {
for (i in 0 until res.length()) { val element = res.getJSONObject(i)
val element = res.getJSONObject(i) if (element.getInt("duration") < 0) continue
if (element.getInt("duration") < 0) continue element.put("uploader", element.getString("uploaderName"))
element.put("uploader", element.getString("uploaderName")) val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url")) if (v == null || v.thumb.isEmpty()) continue
if (v == null || v.thumb.isEmpty()) continue items.add(v)
items.add(v)
}
} catch (e: Exception) {
e.printStackTrace()
} }
return items return items
} }
@ -440,51 +434,43 @@ class InfoUtil(private val context: Context) {
} }
private fun getFormatsFromYTDL(url: String) : List<Format> { private fun getFormatsFromYTDL(url: String) : List<Format> {
try { val request = YoutubeDLRequest(url)
val request = YoutubeDLRequest(url) request.addOption("--print", "%(formats)s")
request.addOption("--print", "%(formats)s") request.addOption("--print", "%(duration)s")
request.addOption("--print", "%(duration)s") request.addOption("--skip-download")
request.addOption("--skip-download") request.addOption("-R", "1")
request.addOption("-R", "1") request.addOption("--socket-timeout", "5")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) 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 proxy = sharedPreferences.getString("proxy", "") val useHeader = sharedPreferences.getBoolean("use_header", false)
if (proxy!!.isNotBlank()) { val header = sharedPreferences.getString("useragent_header", "")
request.addOption("--proxy", proxy) if (useHeader && !header.isNullOrBlank()){
} request.addOption("--add-header","User-Agent:${header}")
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()
} }
} }
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){ 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?> { fun getFromYTDL(query: String): ArrayList<ResultItem?> {
val items = arrayListOf<ResultItem?>() val items = arrayListOf<ResultItem?>()
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch") val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
try {
val request : YoutubeDLRequest val request : YoutubeDLRequest
if (query.contains("http")){ if (query.contains("http")){
request = YoutubeDLRequest(query) 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{ }else{
request = YoutubeDLRequest(emptyList()) request.addOption("${searchEngine}25:\"${query}\"")
if (searchEngine == "ytsearchmusic"){ }
request.addOption("--default-search", "https://music.youtube.com/search?q=") }
request.addOption("ytsearch25:\"${query}\"")
}else{ request.addOption("--flat-playlist")
request.addOption("${searchEngine}25:\"${query}\"") 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") val useHeader = sharedPreferences.getBoolean("use_header", false)
request.addOption("-j") val header = sharedPreferences.getString("useragent_header", "")
request.addOption("--skip-download") if (useHeader && !header.isNullOrBlank()){
request.addOption("-R", "1") request.addOption("--add-header","User-Agent:${header}")
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 proxy = sharedPreferences.getString("proxy", "") val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){ if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy) request.addOption("--proxy", proxy)
} }
val youtubeDLResponse = YoutubeDL.getInstance().execute(request) val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val results: List<String?> = try { val results: List<String?> = try {
val lineSeparator = System.getProperty("line.separator") val lineSeparator = System.getProperty("line.separator")
youtubeDLResponse.out.split(lineSeparator!!) 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
)
)
}
} catch (e: Exception) { } catch (e: Exception) {
val intent = Intent(context, ErrorDialogActivity::class.java) listOf(youtubeDLResponse.out)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK }.filter { it.isNotBlank() }
intent.putExtra("title", context.resources.getString(R.string.no_results)) for (result in results) {
intent.putExtra("message", e.message) if (result.isNullOrBlank()) continue
context.startActivity(intent) val jsonObject = JSONObject(result)
e.printStackTrace() 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 return items
} }
@ -748,89 +726,81 @@ class InfoUtil(private val context: Context) {
} }
fun getMissingInfo(url: String): ResultItem? { fun getMissingInfo(url: String): ResultItem? {
try { val request = YoutubeDLRequest(url)
val request = YoutubeDLRequest(url) request.addOption("--flat-playlist")
request.addOption("--flat-playlist") request.addOption("-J")
request.addOption("-J") request.addOption("--skip-download")
request.addOption("--skip-download") request.addOption("-R", "1")
request.addOption("-R", "1") request.addOption("--socket-timeout", "5")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) 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", "")
val proxy = sharedPreferences.getString("proxy", "") if (useHeader && !header.isNullOrBlank()){
if (proxy!!.isNotBlank()){ request.addOption("--add-header","User-Agent:${header}")
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){
"[${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 val downDir : File
if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){ if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){
downDir = File(FileUtil.formatPath(downloadItem.downloadPath)) downDir = File(FileUtil.formatPath(downloadItem.downloadPath))
request.addOption("--no-quiet")
request.addOption("-no-simulate")
request.addOption("--print", "after_video:'%(filename)s'") request.addOption("--print", "after_video:'%(filename)s'")
request.addOption("--progress")
request.addOption("-v")
}else{ }else{
val cacheDir = FileUtil.getCachePath(context) val cacheDir = FileUtil.getCachePath(context)
downDir = File(cacheDir, downloadItem.id.toString()) downDir = File(cacheDir, downloadItem.id.toString())

View file

@ -2,16 +2,18 @@ package com.deniscerri.ytdlnis.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.Dialog
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape import android.graphics.drawable.shapes.OvalShape
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Looper
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.text.format.DateFormat import android.text.format.DateFormat
@ -34,22 +36,32 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider 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.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView 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.R
import com.deniscerri.ytdlnis.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.TemplateShortcut import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel 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.deniscerri.ytdlnis.ui.downloadcard.VideoCutListener
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog 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.DateValidatorPointForward
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.materialswitch.MaterialSwitch
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.android.material.timepicker.MaterialTimePicker 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.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme import com.neo.highlight.util.scheme.ColorScheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.zhanghai.android.fastscroll.FastScrollerBuilder import me.zhanghai.android.fastscroll.FastScrollerBuilder
@ -416,17 +431,17 @@ object UiUtil {
author!!.text = item.author.ifEmpty { "`${context.getString(R.string.defaultValue)}`" } author!!.text = item.author.ifEmpty { "`${context.getString(R.string.defaultValue)}`" }
// BUTTON ---------------------------------- // 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) { when (item.type) {
DownloadViewModel.Type.audio -> { DownloadViewModel.Type.audio -> {
btn!!.icon = ContextCompat.getDrawable(context, R.drawable.ic_music) btn?.setImageResource(R.drawable.ic_music)
} }
DownloadViewModel.Type.video -> { DownloadViewModel.Type.video -> {
btn!!.icon = ContextCompat.getDrawable(context, R.drawable.ic_video) btn?.setImageResource(R.drawable.ic_video)
} }
else -> { 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){ fun showFormatDetails(format: Format, activity: Activity){
val bottomSheet = BottomSheetDialog(activity) val bottomSheet = BottomSheetDialog(activity)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
@ -653,16 +797,16 @@ object UiUtil {
builder.setView(inputLayout) builder.setView(inputLayout)
builder.setPositiveButton( builder.setPositiveButton(
context.getString(R.string.ok) context.getString(R.string.ok)
) { dialog: DialogInterface?, which: Int -> ) { _: DialogInterface?, _: Int ->
ok(editText.text.toString()) ok(editText.text.toString())
} }
// handle the negative button of the alert dialog // handle the negative button of the alert dialog
builder.setNegativeButton( builder.setNegativeButton(
context.getString(R.string.cancel) context.getString(R.string.cancel)
) { dialog: DialogInterface?, which: Int -> } ) { _: DialogInterface?, _: Int -> }
builder.setNeutralButton("?") { dialog: DialogInterface?, which: Int -> builder.setNeutralButton("?") { _: DialogInterface?, _: Int ->
val browserIntent = val browserIntent =
Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/yt-dlp/yt-dlp#subtitle-options")) Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/yt-dlp/yt-dlp#subtitle-options"))
context.startActivity(browserIntent) context.startActivity(browserIntent)
@ -670,7 +814,7 @@ object UiUtil {
val dialog = builder.create() val dialog = builder.create()
editText.doOnTextChanged { text, start, before, count -> editText.doOnTextChanged { _, _, _, _ ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty() dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
} }
dialog.show() dialog.show()
@ -923,35 +1067,14 @@ object UiUtil {
val filenameTemplate = view.findViewById<Chip>(R.id.filename_template) val filenameTemplate = view.findViewById<Chip>(R.id.filename_template)
filenameTemplate.setOnClickListener { filenameTemplate.setOnClickListener {
val builder = MaterialAlertDialogBuilder(context) val currentFilename = if (items.size == 1 || items.all { it.customFileNameTemplate == items[0].customFileNameTemplate }){
builder.setTitle(context.getString(R.string.file_name_template)) items[0].customFileNameTemplate
val inputLayout = context.layoutInflater.inflate(R.layout.textinput, null) }else {
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)
} }
editText.setSelection(editText.text.length) showFilenameTemplateDialog(context, currentFilename) {
builder.setView(inputLayout) filenameTemplateSet(it)
builder.setPositiveButton(
context.getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
filenameTemplateSet(editText.text.toString())
} }
// 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) val filenameTemplate = view.findViewById<Chip>(R.id.filename_template)
filenameTemplate.setOnClickListener { filenameTemplate.setOnClickListener {
val builder = MaterialAlertDialogBuilder(context) val currentFilename = if (items.size == 1 || items.all { it.customFileNameTemplate == items[0].customFileNameTemplate }){
builder.setTitle(context.getString(R.string.file_name_template)) items[0].customFileNameTemplate
val inputLayout = context.layoutInflater.inflate(R.layout.textinput, null) }else {
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)
} }
editText.setSelection(editText.text.length) showFilenameTemplateDialog(context, currentFilename) {
builder.setView(inputLayout) filenameTemplateSet(it)
builder.setPositiveButton(
context.getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
filenameTemplateSet(editText.text.toString())
} }
// 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) val sponsorBlock = view.findViewById<Chip>(R.id.sponsorblock_filters)
@ -1222,9 +1324,6 @@ object UiUtil {
} }
} }
fun EditText.setTextAndRecalculateWidth(t : String){ fun EditText.setTextAndRecalculateWidth(t : String){
val scale = context.resources.displayMetrics.density val scale = context.resources.displayMetrics.density
this.setText(t) this.setText(t)
@ -1239,4 +1338,258 @@ object UiUtil {
} }
this.requestLayout() 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 Toast.LENGTH_LONG
).show() ).show()
} }
val skippedVersions = sharedPreferences.getString("skip_updates", "")?.split(",")?.distinct()?.toMutableList() ?: mutableListOf()
val res = getGithubReleases() val res = getGithubReleases()
if (res.isEmpty()){ if (res.isEmpty()){
@ -67,6 +68,8 @@ class UpdateUtil(var context: Context) {
isInLatest = false isInLatest = false
} }
if (skippedVersions.contains(v.tag_name)) isInLatest = true
if (isInLatest){ if (isInLatest){
result(context.getString(R.string.you_are_in_latest_version)) result(context.getString(R.string.you_are_in_latest_version))
return return
@ -77,6 +80,11 @@ class UpdateUtil(var context: Context) {
.setTitle(v.tag_name) .setTitle(v.tag_name)
.setMessage(v.body) .setMessage(v.body)
.setIcon(R.drawable.ic_update_app) .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 -> } .setNegativeButton(context.resources.getString(R.string.cancel)) { _: DialogInterface?, _: Int -> }
.setPositiveButton(context.resources.getString(R.string.update)) { _: DialogInterface?, _: Int -> .setPositiveButton(context.resources.getString(R.string.update)) { _: DialogInterface?, _: Int ->
runCatching { runCatching {

View file

@ -136,13 +136,10 @@ class DownloadWorker(
) )
if (logDownloads){ runBlocking {
runBlocking { logItem.id = logRepo.insert(logItem)
logItem.id = logRepo.insert(logItem) downloadItem.logID = logItem.id
downloadItem.logID = logItem.id dao.update(downloadItem)
dao.update(downloadItem)
}
} }
val noCache = !sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite() val noCache = !sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()
@ -156,10 +153,8 @@ class DownloadWorker(
line, progress.toInt(), 0, title, line, progress.toInt(), 0, title,
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
) )
if (logDownloads){ CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch { logRepo.update(line, logItem.id)
logRepo.update(line, logItem.id)
}
} }
} }
}.onSuccess { }.onSuccess {
@ -169,7 +164,7 @@ class DownloadWorker(
if (noCache){ if (noCache){
setProgressAsync(workDataOf("progress" to 100, "output" to "Scanning Files", "id" to downloadItem.id)) 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") } .filter { it.startsWith("'/storage") }
.map { it.removePrefix("'") } .map { it.removePrefix("'") }
.map { it.removeSuffix("'\n") } .map { it.removeSuffix("'\n") }
@ -241,6 +236,8 @@ class DownloadWorker(
if (logDownloads){ if (logDownloads){
logRepo.update(it.out, logItem.id) 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_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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 <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

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

View file

@ -22,66 +22,74 @@
app:navigationIcon="@drawable/ic_back" app:navigationIcon="@drawable/ic_back"
android:layout_height="match_parent"/> 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> <HorizontalScrollView
<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
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="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_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content">
app:selectionRequired="false"
app:singleSelection="false">
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/newTemplate" android:id="@+id/sortChip"
style="@style/Widget.Material3.Chip.Assist" style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/new_template" android:text="@string/sort_by"
app:chipIcon="@drawable/ic_plus" /> app:chipIcon="@drawable/ic_down" />
<com.google.android.material.chip.Chip <View style="@style/Divider.Vertical"/>
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" />
</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 </com.google.android.material.chip.ChipGroup>
android:layout_below="@+id/chips_command"
android:id="@+id/template_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
>
</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" <include layout="@layout/history_no_results"

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
@ -17,10 +18,11 @@
android:padding="20dp"> android:padding="20dp">
<LinearLayout <LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" 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_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
@ -43,28 +45,19 @@
</LinearLayout> </LinearLayout>
<LinearLayout <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/downloads_download_button_layout" android:id="@+id/download_button_type"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="horizontal" 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_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/share" />
<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>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

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

View file

@ -1,6 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<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 <item
android:id="@+id/import_clipboard" android:id="@+id/import_clipboard"
android:title="@string/import_from_clipboard" android:title="@string/import_from_clipboard"

View file

@ -100,13 +100,7 @@
android:id="@+id/resultCardDetailsDialog" android:id="@+id/resultCardDetailsDialog"
android:name="com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog" android:name="com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog"
android:label="ResultCardDetailsDialog" /> android:label="ResultCardDetailsDialog" />
<navigation android:id="@+id/settingsNav" <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" />
<fragment <fragment
android:id="@+id/folderSettingsFragment" android:id="@+id/folderSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.FolderSettingsFragment" android:name="com.deniscerri.ytdlnis.ui.more.settings.FolderSettingsFragment"

View file

@ -821,4 +821,111 @@
<item>m4a|mp4a|aac</item> <item>m4a|mp4a|aac</item>
<item>opus</item> <item>opus</item>
</string-array> </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> </resources>

View file

@ -30,6 +30,7 @@
<string name="confirm_delete_history_desc">Delete the entire history, permanently</string> <string name="confirm_delete_history_desc">Delete the entire history, permanently</string>
<string name="confirm_delete_history">Confirm</string> <string name="confirm_delete_history">Confirm</string>
<string name="search_history_hint">Search from History</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="no_results">No Results</string>
<string name="more">More</string> <string name="more">More</string>
<string name="update_app">Check for updates</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">Remember Download Type</string>
<string name="remember_download_type_summary">Select the last opened download type for the current item</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="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> </resources>

View file

@ -37,6 +37,13 @@
app:title="@string/download_over_metered_networks" /> 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 <EditTextPreference
android:icon="@drawable/baseline_network_locked_24" android:icon="@drawable/baseline_network_locked_24"
app:key="proxy" app:key="proxy"
@ -111,10 +118,14 @@
<EditTextPreference <EditTextPreference
app:key="retries" app:key="retries"
android:defaultValue=""
app:useSimpleSummaryProvider="true"
app:title="@string/retries" /> app:title="@string/retries" />
<EditTextPreference <EditTextPreference
app:key="fragment_retries" app:key="fragment_retries"
android:defaultValue=""
app:useSimpleSummaryProvider="true"
app:title="@string/fragment_retries" /> app:title="@string/fragment_retries" />
</PreferenceCategory> </PreferenceCategory>

View file

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

View file

@ -46,6 +46,15 @@
app:key="hide_thumbnails" app:key="hide_thumbnails"
app:title="@string/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> </PreferenceCategory>

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@ buildscript {
coroutineVer = '1.7.3' coroutineVer = '1.7.3'
retrofitVer = "2.9.0" retrofitVer = "2.9.0"
kodeinVer = "7.16.0" kodeinVer = "7.16.0"
navVer = "2.6.0" navVer = "2.7.4"
media3_version = "1.1.1" media3_version = "1.1.1"
agp_version = '8.1.0' agp_version = '8.1.0'
agp_version1 = '7.4.2' agp_version1 = '7.4.2'
@ -34,11 +34,11 @@ buildscript {
} }
plugins { plugins {
id 'com.android.application' version '7.4.2' apply false id 'com.android.application' version '8.1.1' apply false
id 'com.android.library' version '7.4.2' 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.android' version '1.8.10' apply false
id "org.jetbrains.kotlin.plugin.serialization" 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 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 distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists