added multiple pages to settings & more...

Dismiss download multiple sheet when all items are dismissed
Add backup for history, queued downloads, cancelled downloads, cookies, shortcuts, templates
Add long click format to show details in audio video tab
removed check icon from checked state in material cards
Implement format fetching on playlist from invidious
Invert selected items in playlist bottom sheet
Make filename templates for both audio and video and add them as a chip on both tabs
Fixed errors when there is no internet and u ask for the latest version of the app
Fixed "default" format error
This commit is contained in:
deniscerri 2023-05-13 22:17:43 +02:00
parent 510798d492
commit 880abd503e
No known key found for this signature in database
GPG key ID: 95C43D517D830350
62 changed files with 2242 additions and 1324 deletions

View file

@ -58,7 +58,7 @@ android {
buildTypes {
release {
minifyEnabled true
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
debuggable false
signingConfig signingConfigs.debug
@ -178,4 +178,5 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation "com.google.android.exoplayer:exoplayer:2.18.6"
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
}

View file

@ -61,7 +61,6 @@
public <init>(android.content.Context,androidx.work.WorkerParameters);
}
-dontobfuscate
-dontwarn com.google.android.exoplayer2.**
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken

View file

@ -29,15 +29,8 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
instance = this
createNotificationChannels()
PreferenceManager.setDefaultValues(
this,
"root_preferences",
MODE_PRIVATE,
R.xml.root_preferences,
false
)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
applicationScope = CoroutineScope(SupervisorJob())
@ -80,5 +73,6 @@ class App : Application() {
companion object {
private const val TAG = "App"
private lateinit var applicationScope: CoroutineScope
lateinit var instance: App
}
}

View file

@ -123,7 +123,6 @@ class MainActivity : BaseActivity() {
startActivity(intent)
}
R.id.moreFragment -> {
//navController.navigate(R.id.settingsFragment)
val intent = Intent(context, SettingsActivity::class.java)
startActivity(intent)
}

View file

@ -17,6 +17,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.squareup.picasso.Picasso
import java.util.*
@ -119,6 +120,16 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
}
}
fun invertSelected(items: List<ResultItem?>?){
val invertedList = mutableListOf<String>()
items?.forEach {
if (!checkedItems.contains(it!!.url)) invertedList.add(it.url)
}
checkedItems.clear()
checkedItems.addAll(invertedList)
notifyDataSetChanged()
}
fun checkRange(start: Int, end: Int){
checkedItems.clear()
if (start == end ){

View file

@ -3,18 +3,19 @@ package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.DownloadItem
import kotlinx.coroutines.flow.Flow
@Dao
interface DownloadDao {
@Query("SELECT * FROM downloads ORDER BY status")
fun getAllDownloads() : LiveData<List<DownloadItem>>
fun getAllDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloads() : LiveData<List<DownloadItem>>
fun getActiveDownloads() : Flow<List<DownloadItem>>
@Query("SELECT COUNT(*) FROM downloads WHERE status='Active'")
fun getActiveDownloadsCount() : LiveData<Int>
fun getActiveDownloadsCount() : Flow<Int>
@Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloadsList() : List<DownloadItem>
@ -23,16 +24,22 @@ interface DownloadDao {
fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id")
fun getQueuedDownloads() : LiveData<List<DownloadItem>>
fun getQueuedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id")
fun getQueuedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC")
fun getCancelledDownloads() : LiveData<List<DownloadItem>>
fun getCancelledDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC")
fun getCancelledDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloads() : LiveData<List<DownloadItem>>
fun getErroredDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Processing' ORDER BY id DESC")
fun getProcessingDownloads() : LiveData<List<DownloadItem>>
fun getProcessingDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE id=:id LIMIT 1")
fun getDownloadById(id: Long) : DownloadItem

View file

@ -29,6 +29,9 @@ interface HistoryDao {
@Query("SELECT * FROM history")
fun getAllHistory() : LiveData<List<HistoryItem>>
@Query("SELECT * FROM history")
fun getAllHistoryList() : List<HistoryItem>
@Query("SELECT * FROM history WHERE id=:id LIMIT 1")
suspend fun getHistoryItem(id: Int) : HistoryItem

View file

@ -3,14 +3,15 @@ package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.ResultItem
import kotlinx.coroutines.flow.Flow
@Dao
interface ResultDao {
@Query("SELECT * FROM results")
fun getResults() : LiveData<List<ResultItem>>
fun getResults() : Flow<List<ResultItem>>
@Query("SELECT COUNT(id) FROM results")
fun getCount() : LiveData<Int>
fun getCount() : Flow<Int>
@Query("SELECT COUNT(id) FROM results")
fun getCountInt() :Int

View file

@ -3,5 +3,5 @@ package com.deniscerri.ytdlnis.database.models
data class AudioPreferences(
var embedThumb: Boolean = true,
var splitByChapters: Boolean = false,
var sponsorBlockFilters: ArrayList<String>
var sponsorBlockFilters: ArrayList<String> = arrayListOf()
)

View file

@ -4,7 +4,7 @@ data class VideoPreferences (
var embedSubs: Boolean = true,
var addChapters: Boolean = true,
var splitByChapters: Boolean = false,
var sponsorBlockFilters: ArrayList<String>,
var sponsorBlockFilters: ArrayList<String> = arrayListOf(),
var writeSubs: Boolean = false,
var subsLanguages: String = "en.*,.*-orig",
var removeAudio: Boolean = false

View file

@ -5,15 +5,16 @@ import com.deniscerri.ytdlnis.database.Converters
import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem
import kotlinx.coroutines.flow.Flow
class DownloadRepository(private val downloadDao: DownloadDao) {
val allDownloads : LiveData<List<DownloadItem>> = downloadDao.getAllDownloads()
val activeDownloads : LiveData<List<DownloadItem>> = downloadDao.getActiveDownloads()
val activeDownloadsCount : LiveData<Int> = downloadDao.getActiveDownloadsCount()
val queuedDownloads : LiveData<List<DownloadItem>> = downloadDao.getQueuedDownloads()
val cancelledDownloads : LiveData<List<DownloadItem>> = downloadDao.getCancelledDownloads()
val erroredDownloads : LiveData<List<DownloadItem>> = downloadDao.getErroredDownloads()
val processingDownloads : LiveData<List<DownloadItem>> = downloadDao.getProcessingDownloads()
val allDownloads : Flow<List<DownloadItem>> = downloadDao.getAllDownloads()
val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads()
val activeDownloadsCount : Flow<Int> = downloadDao.getActiveDownloadsCount()
val queuedDownloads : Flow<List<DownloadItem>> = downloadDao.getQueuedDownloads()
val cancelledDownloads : Flow<List<DownloadItem>> = downloadDao.getCancelledDownloads()
val erroredDownloads : Flow<List<DownloadItem>> = downloadDao.getErroredDownloads()
val processingDownloads : Flow<List<DownloadItem>> = downloadDao.getProcessingDownloads()
enum class Status {
Active, Queued, Error, Processing, Cancelled
@ -53,6 +54,14 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getActiveAndQueuedDownloadsList()
}
fun getQueuedDownloads() : List<DownloadItem> {
return downloadDao.getQueuedDownloadsList();
}
fun getCancelledDownloads() : List<DownloadItem> {
return downloadDao.getCancelledDownloadsList()
}
suspend fun deleteCancelled(){
downloadDao.deleteCancelled()
}

View file

@ -19,6 +19,10 @@ class HistoryRepository(private val historyDao: HistoryDao) {
return historyDao.getHistoryItem(id)
}
fun getAll() : List<HistoryItem> {
return historyDao.getAllHistoryList()
}
fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: HistorySort) : List<HistoryItem> {
return when(sortType){
HistorySortType.DATE -> historyDao.getHistorySortedByID(query, format, site, sort.toString())

View file

@ -9,10 +9,11 @@ import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.util.InfoUtil
import kotlinx.coroutines.flow.Flow
class ResultRepository(private val resultDao: ResultDao, private val commandTemplateDao: CommandTemplateDao, private val context: Context) {
private val tag: String = "ResultRepository"
val allResults : LiveData<List<ResultItem>> = resultDao.getResults()
val allResults : Flow<List<ResultItem>> = resultDao.getResults()
var itemCount = MutableLiveData(-1)
suspend fun insert(it: ResultItem){

View file

@ -8,6 +8,7 @@ import android.net.ConnectivityManager
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
import androidx.work.*
@ -52,13 +53,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
allDownloads = repository.allDownloads
queuedDownloads = repository.queuedDownloads
activeDownloads = repository.activeDownloads
activeDownloadsCount = repository.activeDownloadsCount
processingDownloads = repository.processingDownloads
cancelledDownloads = repository.cancelledDownloads
erroredDownloads = repository.erroredDownloads
allDownloads = repository.allDownloads.asLiveData()
queuedDownloads = repository.queuedDownloads.asLiveData()
activeDownloads = repository.activeDownloads.asLiveData()
activeDownloadsCount = repository.activeDownloadsCount.asLiveData()
processingDownloads = repository.processingDownloads.asLiveData()
cancelledDownloads = repository.cancelledDownloads.asLiveData()
erroredDownloads = repository.erroredDownloads.asLiveData()
videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString()
formatIDPreference = sharedPreferences.getString("format_id", "").toString()
@ -114,7 +115,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val addChapters = sharedPreferences.getBoolean("add_chapters", false)
val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
val customFileNameTemplate = sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s")
val customFileNameTemplate = when(type) {
Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)s - %(title)s")
Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s")
else -> ""
}
val downloadPath = when(type){
Type.audio -> sharedPreferences.getString("music_path", getApplication<App>().resources.getString(R.string.music_path))
@ -205,7 +210,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val addChapters = sharedPreferences.getBoolean("add_chapters", false)
val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
val customFileNameTemplate = sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s")
val customFileNameTemplate = when(historyItem.type) {
Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)s - %(title)s")
Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s")
else -> ""
}
val downloadPath = when(historyItem.type){
Type.audio -> sharedPreferences.getString("music_path", getApplication<App>().resources.getString(R.string.music_path))
@ -345,6 +354,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.cancelQueued()
}
fun getQueued() : List<DownloadItem> {
return repository.getQueuedDownloads()
}
fun getCancelled() : List<DownloadItem> {
return repository.getCancelledDownloads()
}
private fun cloneFormat(item: Format) : Format {
val string = Gson().toJson(item, Format::class.java)
return Gson().fromJson(string, Format::class.java)

View file

@ -48,7 +48,6 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
return _items
}
fun setSorting(sort: HistorySortType){
if (sortType.value != sort){
sortOrder.value = HistorySort.DESC
@ -76,6 +75,10 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
_items.postValue(repository.getFiltered(query, format, site, sortType, sort))
}
fun getAll() : List<HistoryItem> {
return repository.getAll()
}
fun insert(item: HistoryItem) = viewModelScope.launch(Dispatchers.IO){
repository.insert(item)
}

View file

@ -7,6 +7,7 @@ import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.App
@ -35,7 +36,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
val commandDao = DBManager.getInstance(application).commandTemplateDao
repository = ResultRepository(dao, commandDao, getApplication<Application>().applicationContext)
searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao)
items = repository.allResults
items = repository.allResults.asLiveData()
loadingItems.postValue(false)
itemCount = repository.itemCount
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)

View file

@ -21,12 +21,14 @@ import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.DefaultRenderersFactory
import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem.fromUri
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
@ -131,9 +133,18 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val list
val trackSelector = DefaultTrackSelector(requireContext())
val loadControl = DefaultLoadControl()
player = ExoPlayer.Builder(requireContext(), renderersFactory)
player = ExoPlayer.Builder(requireContext())
.setUsePlatformDiagnostics(false)
.setTrackSelector(trackSelector)
.setLoadControl(loadControl)
.setHandleAudioBecomingNoisy(true)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build()
,
true)
.build()
val frame = view.findViewById<MaterialCardView>(R.id.frame_layout)
val videoView = view.findViewById<StyledPlayerView>(R.id.video_view)
@ -207,9 +218,8 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val list
progress.visibility = View.GONE
populateSuggestedChapters()
player.prepare().apply {
player.play()
}
player.prepare()
player.play()
}catch (e: Exception){
progress.visibility = View.GONE
frame.visibility = View.GONE

View file

@ -3,18 +3,25 @@ package com.deniscerri.ytdlnis.ui.downloadcard
import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.EditText
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -142,10 +149,6 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
override fun onFormatClick(allFormats: List<List<Format>>, item: List<Format>) {
downloadItem.format = item.first()
uiUtil.populateFormatCard(formatCard, item.first())
if (containers.contains(item.first().container)){
downloadItem.format.container = item.first().container
containerAutoCompleteTextView.setText(item.first().container, false)
}
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultItem.formats.removeAll(formats.toSet())
@ -162,6 +165,10 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
bottomSheet.show(parentFragmentManager, "formatSheet")
}
}
formatCard.setOnLongClickListener {
uiUtil.showFormatDetails(downloadItem.format, requireActivity())
true
}
container?.isEnabled = true
@ -173,12 +180,13 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
)
)
downloadItem.format.container = containerPreference!!
containerAutoCompleteTextView.setText(downloadItem.format.container, false)
downloadItem.format.container = if (containerPreference == getString(R.string.defaultValue)) "" else containerPreference!!
containerAutoCompleteTextView.setText(downloadItem.format.container.ifEmpty { getString(R.string.defaultValue) }, false)
(container!!.editText as AutoCompleteTextView?)!!.onItemClickListener =
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, index: Int, _: Long ->
downloadItem.format.container = containers[index]
if (containers[index] == getString(R.string.defaultValue)) downloadItem.format.container = ""
}
val embedThumb = view.findViewById<Chip>(R.id.embed_thumb)
@ -199,6 +207,41 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
downloadItem.audioPreferences.splitByChapters = splitByChapters.isChecked
}
val filenameTemplate = view.findViewById<Chip>(R.id.filename_template)
filenameTemplate.setOnClickListener {
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.file_name_template))
val inputLayout = layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = getString(R.string.file_name_template)
editText.setText(downloadItem.customFileNameTemplate)
editText.setSelection(editText.text.length)
builder.setView(inputLayout)
builder.setPositiveButton(
getString(R.string.ok)
) { dialog: DialogInterface?, which: Int ->
downloadItem.customFileNameTemplate = editText.text.toString()
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
getString(R.string.cancel)
) { dialog: DialogInterface?, which: Int -> }
val dialog = builder.create()
editText.doOnTextChanged { text, start, before, count ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
}
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_POSITIVE).isEnabled = editText.text.isNotEmpty()
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
val sponsorblock = view.findViewById<Chip>(R.id.sponsorblock_filters)
sponsorblock!!.setOnClickListener {
val builder = MaterialAlertDialogBuilder(requireContext())

View file

@ -71,7 +71,9 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
if(resources.getBoolean(R.bool.isTablet)){
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}

View file

@ -79,7 +79,9 @@ class DownloadMultipleBottomSheetDialog(private var results: List<ResultItem?>,
behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
if(resources.getBoolean(R.bool.isTablet)){
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
listAdapter =
@ -409,11 +411,16 @@ class DownloadMultipleBottomSheetDialog(private var results: List<ResultItem?>,
val deletedItem = items[position]
items.remove(deletedItem)
listAdapter.submitList(items.toList())
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
items.add(position, deletedItem)
listAdapter.submitList(items.toList())
}.show()
if (items.isNotEmpty()){
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
items.add(position, deletedItem)
listAdapter.submitList(items.toList())
}.show()
}else{
dismiss()
}
}
}

View file

@ -148,11 +148,6 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
val listener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, item: List<Format>) {
downloadItem.format = item.first()
if (containers.contains(item.first().container)){
downloadItem.format.container = item.first().container
containerAutoCompleteTextView.setText(item.first().container, false)
}
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultItem.formats.removeAll(formats.toSet())
@ -171,6 +166,11 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
}
}
formatCard.setOnLongClickListener {
uiUtil.showFormatDetails(downloadItem.format, requireActivity())
true
}
container?.isEnabled = true
containerAutoCompleteTextView?.setAdapter(
ArrayAdapter(
@ -187,6 +187,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
(container!!.editText as AutoCompleteTextView?)!!.onItemClickListener =
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, index: Int, _: Long ->
downloadItem.format.container = containers[index]
if (containers[index] == getString(R.string.defaultValue)) downloadItem.format.container = ""
}
@ -313,6 +314,41 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
cut.isEnabled = false
}
val filenameTemplate = view.findViewById<Chip>(R.id.filename_template)
filenameTemplate.setOnClickListener {
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.file_name_template))
val inputLayout = layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = getString(R.string.file_name_template)
editText.setText(downloadItem.customFileNameTemplate)
editText.setSelection(editText.text.length)
builder.setView(inputLayout)
builder.setPositiveButton(
getString(R.string.ok)
) { dialog: DialogInterface?, which: Int ->
downloadItem.customFileNameTemplate = editText.text.toString()
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
getString(R.string.cancel)
) { dialog: DialogInterface?, which: Int -> }
val dialog = builder.create()
editText.doOnTextChanged { text, start, before, count ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty()
}
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_POSITIVE).isEnabled = editText.text.isNotEmpty()
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
val saveSubtitles = view.findViewById<Chip>(R.id.save_subtitles)
val subtitleLanguages = view.findViewById<Chip>(R.id.subtitle_languages)
if (downloadItem.videoPreferences.writeSubs) {

View file

@ -210,110 +210,13 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
dismiss()
}
formatItem.setOnLongClickListener {
val bottomSheet = BottomSheetDialog(requireContext())
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.format_details_sheet)
val formatIdParent = bottomSheet.findViewById<LinearLayout>(R.id.format_id_parent)
val containerParent = bottomSheet.findViewById<LinearLayout>(R.id.container_parent)
val codecParent = bottomSheet.findViewById<LinearLayout>(R.id.codec_parent)
val filesizeParent = bottomSheet.findViewById<LinearLayout>(R.id.filesize_parent)
val formatnoteParent = bottomSheet.findViewById<LinearLayout>(R.id.format_note_parent)
val fpsParent = bottomSheet.findViewById<LinearLayout>(R.id.fps_parent)
val asrParent = bottomSheet.findViewById<LinearLayout>(R.id.asr_parent)
if (format.format_id.isBlank()) formatIdParent?.visibility = View.GONE
else {
formatIdParent?.findViewById<TextView>(R.id.format_id_value)?.text = format.format_id
formatIdParent?.setOnClickListener {
copyToClipboard(format.format_id)
}
}
if (format.container.isBlank()) containerParent?.visibility = View.GONE
else {
containerParent?.findViewById<TextView>(R.id.container_value)?.text = format.container
containerParent?.setOnClickListener {
copyToClipboard(format.container)
}
}
val codecField =
if (format.encoding != "") {
format.encoding.uppercase()
}else if (format.vcodec != "none" && format.vcodec != ""){
format.vcodec.uppercase()
} else {
format.acodec.uppercase()
}
if (codecField.isBlank()) codecParent?.visibility = View.GONE
else {
codecParent?.findViewById<TextView>(R.id.codec_value)?.text = codecField
codecParent?.setOnClickListener {
copyToClipboard(codecField)
}
}
if (format.filesize != 0L) filesizeParent?.visibility = View.GONE
else {
filesizeParent?.findViewById<TextView>(R.id.filesize_value)?.text = fileUtil.convertFileSize(format.filesize)
filesizeParent?.setOnClickListener {
copyToClipboard(fileUtil.convertFileSize(format.filesize))
}
}
if (format.format_note.isBlank()) formatnoteParent?.visibility = View.GONE
else {
formatnoteParent?.findViewById<TextView>(R.id.format_note_value)?.text = format.format_note
formatnoteParent?.setOnClickListener {
copyToClipboard(format.format_note)
}
}
if (format.fps.isNullOrBlank()) fpsParent?.visibility = View.GONE
else {
fpsParent?.findViewById<TextView>(R.id.fps_value)?.text = format.fps
fpsParent?.setOnClickListener {
copyToClipboard(format.fps!!)
}
}
if (format.asr.isNullOrBlank()) asrParent?.visibility = View.GONE
else {
asrParent?.findViewById<TextView>(R.id.asr_value)?.text = format.asr
asrParent?.setOnClickListener {
copyToClipboard(format.asr!!)
}
}
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
)
uiUtil.showFormatDetails(format, requireActivity())
true
}
linearLayout.addView(formatItem)
}
}
private fun copyToClipboard(text: String){
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(text, text)
clipboard.setPrimaryClip(clip)
Toast.makeText(context, requireContext().getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT)
.show()
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
cleanUp()

View file

@ -2,13 +2,16 @@ package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.ClipboardManager
import android.content.DialogInterface
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -21,8 +24,10 @@ import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.receiver.ShareActivity
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers
@ -115,12 +120,14 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
}
}
val checkAll = view.findViewById<FloatingActionButton>(R.id.check_all)
val checkAll = view.findViewById<ExtendedFloatingActionButton>(R.id.check_all)
checkAll!!.setOnClickListener {
if (listAdapter.getCheckedItems().size != items.size){
fromTextInput.editText!!.setText("1")
toTextInput.editText!!.setText(items.size.toString())
listAdapter.checkAll()
fromTextInput.isEnabled = true
toTextInput.isEnabled = true
selectedText.text = resources.getString(R.string.all_items_selected)
}else{
reset()
@ -160,6 +167,24 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
}
}
view.findViewById<BottomAppBar>(R.id.bottomAppBar).setOnMenuItemClickListener { m: MenuItem ->
val itemId = m.itemId
if (itemId == R.id.invert_selected) {
listAdapter.invertSelected(items)
val checkedItems = listAdapter.getCheckedItems()
if (checkedItems.size == items.size){
selectedText.text = resources.getString(R.string.all_items_selected)
}else{
selectedText.text = "${checkedItems.size} ${resources.getString(R.string.selected)}"
}
if(checkedItems.isNotEmpty() && checkedItems.size < items.size){
fromTextInput.isEnabled = false
toTextInput.isEnabled = false
}
}
true
}
}
override fun onCancel(dialog: DialogInterface) {

View file

@ -32,6 +32,7 @@ import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -124,6 +125,7 @@ class CookiesActivity : BaseActivity(), CookieAdapter.OnItemClickListener {
builder.setTitle(getString(R.string.cookies))
val inputLayout = layoutInflater.inflate(R.layout.textinput, null)
val editText = inputLayout.findViewById<EditText>(R.id.url_edittext)
inputLayout.findViewById<TextInputLayout>(R.id.url_textinput).hint = "URL"
val text = if (url.isNullOrBlank()) "https://" else url
editText.setText(text)
editText.setSelection(editText.text.length)

View file

@ -0,0 +1,158 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context.POWER_SERVICE
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.os.Environment
import android.os.PowerManager
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.preference.SeekBarPreference
import androidx.preference.SwitchPreferenceCompat
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.SearchHistoryItem
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.time.LocalDateTime
import java.util.Locale
class AppearanceSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.appearance
private var language: ListPreference? = null
private var theme: ListPreference? = null
private var accent: ListPreference? = null
private var highContrast: SwitchPreferenceCompat? = null
private var locale: ListPreference? = null
private var updateUtil: UpdateUtil? = null
private var fileUtil: FileUtil? = null
private var activeDownloadCount = 0
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.appearance_preferences, rootKey)
updateUtil = UpdateUtil(requireContext())
fileUtil = FileUtil()
WorkManager.getInstance(requireContext()).getWorkInfosByTagLiveData("download").observe(this){
activeDownloadCount = 0
it.forEach {w ->
if (w.state == WorkInfo.State.RUNNING) activeDownloadCount++
}
}
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
language = findPreference("app_language")
theme = findPreference("ytdlnis_theme")
accent = findPreference("theme_accent")
highContrast = findPreference("high_contrast")
locale = findPreference("locale")
if(VERSION.SDK_INT < VERSION_CODES.TIRAMISU){
val values = resources.getStringArray(R.array.language_values)
val entries = mutableListOf<String>()
values.forEach {
entries.add(Locale(it).getDisplayName(Locale(it)))
}
language!!.entries = entries.toTypedArray()
}else{
language!!.isVisible = false
}
if(language!!.value == null) language!!.value = Locale.getDefault().language
language!!.summary = Locale(language!!.value).getDisplayLanguage(Locale(language!!.value))
language!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
language!!.summary = Locale(newValue.toString()).getDisplayLanguage(Locale(newValue.toString()))
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(newValue.toString()))
true
}
theme!!.summary = theme!!.entry
theme!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
when(newValue){
"System" -> {
theme!!.summary = getString(R.string.system)
}
"Dark" -> {
theme!!.summary = getString(R.string.dark)
}
else -> {
theme!!.summary = getString(R.string.light)
}
}
ThemeUtil.updateTheme(requireActivity() as AppCompatActivity)
val intent = Intent(requireContext(), MainActivity::class.java)
this.startActivity(intent)
requireActivity().finishAffinity()
true
}
accent!!.summary = accent!!.entry
accent!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, _: Any ->
ThemeUtil.updateTheme(requireActivity() as AppCompatActivity)
val intent = Intent(requireContext(), MainActivity::class.java)
this.startActivity(intent)
requireActivity().finishAffinity()
true
}
highContrast!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, _: Any ->
ThemeUtil.updateTheme(requireActivity() as AppCompatActivity)
val intent = Intent(requireContext(), MainActivity::class.java)
this.startActivity(intent)
requireActivity().finishAffinity()
true
}
}
}

View file

@ -0,0 +1,13 @@
package com.deniscerri.ytdlnis.ui.more.settings
import androidx.preference.PreferenceFragmentCompat
abstract class BaseSettingsFragment : PreferenceFragmentCompat() {
abstract val title: Int
override fun onStart() {
super.onStart()
(activity as? SettingsActivity)?.changeTopAppbarTitle(getString(title))
}
}

View file

@ -0,0 +1,67 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.app.Activity
import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import androidx.preference.Preference
import androidx.preference.SeekBarPreference
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class DownloadSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.downloading
private var concurrentDownloads: SeekBarPreference? = null
private var ignoreBatteryOptimization: Preference? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.downloading_preferences, rootKey)
ignoreBatteryOptimization = findPreference("ignore_battery")
val packageName: String = requireContext().packageName
val pm = requireContext().applicationContext.getSystemService(POWER_SERVICE) as PowerManager
if (pm.isIgnoringBatteryOptimizations(packageName)) {
ignoreBatteryOptimization!!.isVisible = false
}
concurrentDownloads = findPreference("concurrent_downloads")
concurrentDownloads!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, _: Any ->
MaterialAlertDialogBuilder(requireContext())
.setTitle(resources.getString(R.string.warning))
.setMessage(resources.getString(R.string.workmanager_updated))
.setNegativeButton(resources.getString(R.string.cancel)) { dialog, _ ->
dialog.cancel()
}
.setPositiveButton(resources.getString(R.string.restart)) { _, _ ->
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
requireContext().startActivity(intent)
if (context is Activity) {
(context as Activity).finish()
}
Runtime.getRuntime().exit(0)
}
.show()
true
}
ignoreBatteryOptimization = findPreference("ignore_battery")
ignoreBatteryOptimization!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent()
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:" + requireContext().packageName)
startActivity(intent)
true
}
}
}

View file

@ -0,0 +1,179 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build.VERSION
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.snackbar.Snackbar
import java.io.File
class FolderSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.directories
private var musicPath: Preference? = null
private var videoPath: Preference? = null
private var commandPath: Preference? = null
private var accessAllFiles : Preference? = null
private var clearCache: Preference? = null
private var fileUtil: FileUtil? = null
private var activeDownloadCount = 0
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.folders_preference, rootKey)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = preferences.edit()
fileUtil = FileUtil()
musicPath = findPreference("music_path")
videoPath = findPreference("video_path")
commandPath = findPreference("command_path")
accessAllFiles = findPreference("access_all_files")
clearCache = findPreference("clear_cache")
if (preferences.getString("music_path", "")!!.isEmpty()) {
editor.putString("music_path", getString(R.string.music_path))
}
if (preferences.getString("video_path", "")!!.isEmpty()) {
editor.putString("video_path", getString(R.string.video_path))
}
if (preferences.getString("command_path", "")!!.isEmpty()) {
editor.putString("command_path", getString(R.string.command_path))
}
if((VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) ||
VERSION.SDK_INT < 30) {
accessAllFiles!!.isVisible = false
}
editor.apply()
musicPath!!.summary = fileUtil?.formatPath(preferences.getString("music_path", "")!!)
musicPath!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
musicPathResultLauncher.launch(intent)
true
}
videoPath!!.summary = fileUtil?.formatPath(preferences.getString("video_path", "")!!)
videoPath!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
videoPathResultLauncher.launch(intent)
true
}
commandPath!!.summary = fileUtil?.formatPath(preferences.getString("command_path", "")!!)
commandPath!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
commandPathResultLauncher.launch(intent)
true
}
accessAllFiles!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = Uri.parse("package:" + requireContext().packageName)
intent.data = uri
startActivity(intent)
true
}
var cacheSize = File(requireContext().cacheDir.absolutePath + "/downloads").walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
clearCache!!.summary = "(${fileUtil!!.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}"
clearCache!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
if (activeDownloadCount == 0){
File(requireContext().cacheDir.absolutePath + "/downloads").deleteRecursively()
Snackbar.make(requireView(), getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show()
cacheSize = File(requireContext().cacheDir.absolutePath + "/downloads").walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
clearCache!!.summary = "(${fileUtil!!.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}"
}else{
Snackbar.make(requireView(), getString(R.string.downloads_running_try_later), Snackbar.LENGTH_SHORT).show()
}
true
}
}
private var musicPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(musicPath, result.data, MUSIC_PATH_CODE)
}
}
private var videoPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(videoPath, result.data, VIDEO_PATH_CODE)
}
}
private var commandPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(commandPath, result.data, COMMAND_PATH_CODE)
}
}
private fun changePath(p: Preference?, data: Intent?, requestCode: Int) {
val path = data!!.data.toString()
p!!.summary = fileUtil?.formatPath(data.data.toString())
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = sharedPreferences.edit()
when (requestCode) {
MUSIC_PATH_CODE -> editor.putString("music_path", path)
VIDEO_PATH_CODE -> editor.putString("video_path", path)
COMMAND_PATH_CODE -> editor.putString("command_path", path)
}
editor.apply()
}
companion object {
const val MUSIC_PATH_CODE = 33333
const val VIDEO_PATH_CODE = 55555
const val COMMAND_PATH_CODE = 77777
}
}

View file

@ -0,0 +1,501 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.os.Environment
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.SearchHistoryItem
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.util.Calendar
class MainSettingsFragment : PreferenceFragmentCompat() {
private var backup : Preference? = null
private var restore : Preference? = null
private var updateUtil: UpdateUtil? = null
private var fileUtil: FileUtil? = null
private var activeDownloadCount = 0
private lateinit var resultViewModel: ResultViewModel
private lateinit var historyViewModel: HistoryViewModel
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var cookieViewModel: CookieViewModel
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
val navController = findNavController()
val appearance = findPreference<Preference>("appearance")
appearance?.summary = "${getString(R.string.language)}, ${getString(R.string.Theme)}, ${getString(R.string.accents)}"
appearance?.setOnPreferenceClickListener {
navController.navigate(R.id.action_mainSettingsFragment_to_appearanceSettingsFragment)
true
}
val folders = findPreference<Preference>("folders")
folders?.summary = "${getString(R.string.music_directory)}, ${getString(R.string.video_directory)}, ${getString(R.string.command_directory)}"
folders?.setOnPreferenceClickListener {
navController.navigate(R.id.action_mainSettingsFragment_to_folderSettingsFragment)
true
}
val downloading = findPreference<Preference>("downloading")
downloading?.summary = "${getString(R.string.quick_download)}, ${getString(R.string.concurrent_downloads)}, ${getString(R.string.limit_rate)}"
downloading?.setOnPreferenceClickListener {
navController.navigate(R.id.action_mainSettingsFragment_to_downloadSettingsFragment)
true
}
val processing = findPreference<Preference>("processing")
processing?.summary = "${getString(R.string.sponsorblock)}, ${getString(R.string.embed_subtitles)}, ${getString(R.string.add_chapters)}"
processing?.setOnPreferenceClickListener {
navController.navigate(R.id.action_mainSettingsFragment_to_processingSettingsFragment)
true
}
val updating = findPreference<Preference>("updating")
updating?.summary = "${getString(R.string.update_ytdl)}, ${getString(R.string.format_source)}, ${getString(R.string.update_app)}"
updating?.setOnPreferenceClickListener {
navController.navigate(R.id.action_mainSettingsFragment_to_updateSettingsFragment)
true
}
//about section -------------------------
updateUtil = UpdateUtil(requireContext())
fileUtil = FileUtil()
WorkManager.getInstance(requireContext()).getWorkInfosByTagLiveData("download").observe(this){
activeDownloadCount = 0
it.forEach {w ->
if (w.state == WorkInfo.State.RUNNING) activeDownloadCount++
}
}
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
backup = findPreference("backup")
restore = findPreference("restore")
backup!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.select_backup_categories))
val values = resources.getStringArray(R.array.backup_category_values)
val entries = resources.getStringArray(R.array.backup_category_entries)
val checkedItems : ArrayList<Boolean> = arrayListOf()
values.forEach { _ ->
checkedItems.add(true)
}
builder.setMultiChoiceItems(
entries,
checkedItems.toBooleanArray()
) { _, which, isChecked ->
checkedItems[which] = isChecked
}
builder.setPositiveButton(
getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
lifecycleScope.launch(Dispatchers.IO) {
val json = JsonObject()
json.addProperty("app", "YTDLnis_backup")
for (i in 0 until checkedItems.size) {
if (checkedItems[i]) {
when(values[i]){
"settings" -> json.add("settings", backupSettings(preferences))
"downloads" -> json.add("downloads", backupHistory())
"queued" -> json.add("queued", backupQueuedDownloads() )
"cancelled" -> json.add("cancelled", backupCancelledDownloads() )
"cookies" -> json.add("cookies", backupCookies() )
"templates" -> json.add("templates", backupCommandTemplates() )
"shortcuts" -> json.add("shortcuts", backupShortcuts() )
"searchHistory" -> json.add("search_history", backupSearchHistory() )
}
}
}
val currentTime = Calendar.getInstance()
val saveFile = File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS), "YTDLnis_${BuildConfig.VERSION_NAME}_${currentTime.get(
Calendar.YEAR)}-${currentTime.get(Calendar.MONTH) + 1}-${currentTime.get(
Calendar.DAY_OF_MONTH)}.json")
saveFile.delete()
saveFile.createNewFile()
saveFile.writeText(Gson().toJson(json))
Snackbar.make(requireView(), getString(R.string.backup_created_successfully), Snackbar.LENGTH_LONG).show()
}
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
dialog.show()
true
}
restore!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
}
appRestoreResultLauncher.launch(intent)
true
}
}
private fun backupSettings(preferences: SharedPreferences) : JsonArray {
runCatching {
val prefs = preferences.all
val arr = JsonArray()
prefs.forEach {
val obj = JsonObject()
obj.addProperty("key", it.key)
obj.addProperty("value", it.value.toString())
obj.addProperty("type", it.value!!::class.simpleName)
arr.add(obj)
}
return arr
}
return JsonArray()
}
private suspend fun backupHistory() : JsonArray {
runCatching {
val historyItems = withContext(Dispatchers.IO) {
historyViewModel.getAll()
}
val arr = JsonArray()
historyItems.forEach {
arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupQueuedDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getQueued()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupCancelledDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getCancelled()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupCookies() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
cookieViewModel.getAll()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupCommandTemplates() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
commandTemplateViewModel.getAll()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupShortcuts() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
commandTemplateViewModel.getAllShortcuts()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupSearchHistory() : JsonArray {
runCatching {
val historyItems = withContext(Dispatchers.IO) {
resultViewModel.getSearchHistory()
}
val arr = JsonArray()
historyItems.forEach {
arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private var appRestoreResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
lifecycleScope.launch {
val preferences =
PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = preferences.edit()
runCatching {
val ip = requireContext().contentResolver.openInputStream(result.data!!.data!!)
val r = BufferedReader(InputStreamReader(ip))
val total: java.lang.StringBuilder = java.lang.StringBuilder()
var line: String?
while (r.readLine().also { line = it } != null) {
total.append(line).append('\n')
}
val json = Gson().fromJson(total.toString(), JsonObject::class.java)
val finalMessage = StringBuilder("${getString(R.string.restore_complete)}\n")
//preference restore
if(json.has("settings")){
val prefs = json.getAsJsonArray("settings")
val preferencesKeys = preferences.all.map { it.key }
prefs.forEach {
val key : String = it.asJsonObject.get("key").toString().replace("\"", "")
if (preferencesKeys.contains(key)){
when(it.asJsonObject.get("type").toString().replace("\"", "")){
"String" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "")
editor.putString(key, value)
}
"Boolean" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
editor.putBoolean(key, value)
}
"Int" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
editor.putInt(key, value)
}
"HashSet" -> {
val value = hashSetOf(it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])".toRegex(), ""))
editor.putStringSet(key, value)
}
}
}
}
finalMessage.append("${getString(R.string.settings)}: ${prefs.count()}\n")
}
//history restore
if(json.has("downloads")){
val items = json.getAsJsonArray("downloads")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), HistoryItem::class.java)
item.id = 0L
withContext(Dispatchers.IO){
historyViewModel.insert(item)
}
}
finalMessage.append("${getString(R.string.downloads)}: ${items.count()}\n")
}
//queued downloads restore
if(json.has("queued")){
val items = json.getAsJsonArray("queued")
val queued = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
queued.add(item)
}
if (queued.isNotEmpty()) {
finalMessage.append("${getString(R.string.in_queue)}: ${queued.count()}\n")
withContext(Dispatchers.IO){downloadViewModel.queueDownloads(queued)}
}
}
//cancelled downloads restore
if(json.has("cancelled")){
val items = json.getAsJsonArray("cancelled")
val cancelled = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
cancelled.add(item)
}
if(cancelled.isNotEmpty()){
finalMessage.append("${getString(R.string.cancelled)}: ${cancelled.count()}\n")
}
}
//cookies restore
if(json.has("cookies")){
val items = json.getAsJsonArray("cookies")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CookieItem::class.java)
item.id = 0L
withContext(Dispatchers.IO){
cookieViewModel.insert(item)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.cookies)}: ${items.count()}\n")
}
}
//command template restore
if(json.has("templates")){
val items = json.getAsJsonArray("templates")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CommandTemplate::class.java)
item.id = 0L
withContext(Dispatchers.IO){
commandTemplateViewModel.insert(item)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.command_templates)}: ${items.count()}\n")
}
}
//shortcuts restore
if(json.has("shortcuts")){
val items = json.getAsJsonArray("shortcuts")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), TemplateShortcut::class.java)
item.id = 0L
withContext(Dispatchers.IO){
commandTemplateViewModel.insertShortcut(item)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.shortcuts)}: ${items.count()}\n")
}
}
//search history restore
if(json.has("search_history")){
val items = json.getAsJsonArray("search_history")
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), SearchHistoryItem::class.java)
withContext(Dispatchers.IO){
resultViewModel.addSearchQueryToHistory(item.query)
}
}
if(items.count() > 0){
finalMessage.append("${getString(R.string.search_history)}: ${items.count()}\n")
}
}
finalMessage.append("${getString(R.string.restart)}?")
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.restore))
builder.setMessage(finalMessage)
builder.setPositiveButton(
getString(R.string.restart)
) { _: DialogInterface?, _: Int ->
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
requireContext().startActivity(intent)
if (context is Activity) {
(context as Activity).finish()
}
Runtime.getRuntime().exit(0)
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
dialog.show()
}.onFailure {
it.printStackTrace()
Snackbar.make(requireView(), getString(R.string.couldnt_parse_file), Snackbar.LENGTH_LONG).show()
}
}
}
}
}

View file

@ -0,0 +1,20 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.os.Bundle
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import com.deniscerri.ytdlnis.R
class ProcessingSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.processing
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.processing_preferences, rootKey)
findPreference<EditTextPreference>("file_name_template")?.title = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
findPreference<EditTextPreference>("file_name_template")?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
findPreference<EditTextPreference>("file_name_template_audio")?.title = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]"
findPreference<EditTextPreference>("file_name_template_audio")?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]"
}
}

View file

@ -3,28 +3,56 @@ package com.deniscerri.ytdlnis.ui.more.settings
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.activity.addCallback
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.databinding.ActivitySettingsBinding
import com.deniscerri.ytdlnis.ui.BaseActivity
import com.google.android.material.appbar.MaterialToolbar
class SettingsActivity : BaseActivity() {
private var fm: FragmentManager? = null
private var topAppBar: MaterialToolbar? = null
private lateinit var sharedPreferences: SharedPreferences
private lateinit var editor: SharedPreferences.Editor
var context: Context? = null
lateinit var binding: ActivitySettingsBinding
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
context = baseContext
topAppBar = findViewById(R.id.settings_toolbar)
topAppBar!!.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
fm = supportFragmentManager
fm!!.beginTransaction()
.replace(R.id.settings_frame_layout, SettingsFragment())
.commit()
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.frame_layout) as NavHostFragment
val navController = navHostFragment.findNavController()
val listener = NavController.OnDestinationChangedListener { controller, destination, arguments ->
if (destination.id == R.id.mainSettingsFragment){
changeTopAppbarTitle(getString(R.string.settings))
}
}
navController.addOnDestinationChangedListener(listener)
binding.settingsToolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
onBackPressedDispatcher.addCallback(this) {
if (navController.currentDestination?.id == R.id.mainSettingsFragment) {
navController.popBackStack()
finishAndRemoveTask()
}else{
navController.navigateUp()
}
}
navController.navigate(R.id.mainSettingsFragment)
}
fun changeTopAppbarTitle(text: String) {
if (this::binding.isInitialized) binding.collapsingToolbar.title = text
}
}

View file

@ -1,522 +0,0 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipboardManager
import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.net.Uri
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.os.Environment
import android.os.PowerManager
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.preference.SeekBarPreference
import androidx.preference.SwitchPreferenceCompat
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.ThemeUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import java.io.File
import java.util.Locale
class SettingsFragment : PreferenceFragmentCompat() {
private var language: ListPreference? = null
private var theme: ListPreference? = null
private var accent: ListPreference? = null
private var highContrast: SwitchPreferenceCompat? = null
private var ignoreBatteryOptimization: Preference? = null
private var musicPath: Preference? = null
private var videoPath: Preference? = null
private var commandPath: Preference? = null
private var accessAllFiles : Preference? = null
private var clearCache: Preference? = null
private var locale: ListPreference? = null
private var concurrentFragments: SeekBarPreference? = null
private var concurrentDownloads: SeekBarPreference? = null
private var audioFormat: ListPreference? = null
private var videoFormat: ListPreference? = null
private var videoQuality: ListPreference? = null
private var formatID: EditTextPreference? = null
private var updateYTDL: Preference? = null
private var formatSource: ListPreference? = null
private var exportPreferences : Preference? = null
private var importPreferences : Preference? = null
private var ytdlVersion: Preference? = null
private var version: Preference? = null
private var updateUtil: UpdateUtil? = null
private var fileUtil: FileUtil? = null
private var activeDownloadCount = 0
private val jsonFormat = Json { prettyPrint = true }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
updateUtil = UpdateUtil(requireContext())
fileUtil = FileUtil()
WorkManager.getInstance(requireContext()).getWorkInfosByTagLiveData("download").observe(this){
activeDownloadCount = 0
it.forEach {w ->
if (w.state == WorkInfo.State.RUNNING) activeDownloadCount++
}
}
initPreferences()
initListeners()
}
private fun initPreferences() {
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = preferences.edit()
language = findPreference("app_language")
theme = findPreference("ytdlnis_theme")
accent = findPreference("theme_accent")
highContrast = findPreference("high_contrast")
ignoreBatteryOptimization = findPreference("ignore_battery")
musicPath = findPreference("music_path")
videoPath = findPreference("video_path")
commandPath = findPreference("command_path")
accessAllFiles = findPreference("access_all_files")
clearCache = findPreference("clear_cache")
locale = findPreference("locale")
concurrentFragments = findPreference("concurrent_fragments")
concurrentDownloads = findPreference("concurrent_downloads")
audioFormat = findPreference("audio_format")
videoFormat = findPreference("video_format")
videoQuality = findPreference("video_quality")
formatID = findPreference("format_id")
updateYTDL = findPreference("update_ytdl")
formatSource = findPreference("formats_source")
exportPreferences = findPreference("export_preferences")
importPreferences = findPreference("import_preferences")
ytdlVersion = findPreference("ytdl-version")
ytdlVersion!!.summary = preferences.getString("ytdl-version", "NULL")
version = findPreference("version")
version!!.summary = BuildConfig.VERSION_NAME
if(VERSION.SDK_INT < VERSION_CODES.TIRAMISU){
val values = resources.getStringArray(R.array.language_values)
val entries = mutableListOf<String>()
values.forEach {
entries.add(Locale(it).getDisplayName(Locale(it)))
}
language!!.entries = entries.toTypedArray()
}else{
language!!.isVisible = false
}
val packageName: String = requireContext().packageName
val pm = requireContext().applicationContext.getSystemService(POWER_SERVICE) as PowerManager
if (pm.isIgnoringBatteryOptimizations(packageName)) {
ignoreBatteryOptimization!!.isVisible = false
}
if (preferences.getString("music_path", "")!!.isEmpty()) {
editor.putString("music_path", getString(R.string.music_path))
}
if (preferences.getString("video_path", "")!!.isEmpty()) {
editor.putString("video_path", getString(R.string.video_path))
}
if (preferences.getString("command_path", "")!!.isEmpty()) {
editor.putString("command_path", getString(R.string.command_path))
}
if((VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) ||
VERSION.SDK_INT < 30) {
accessAllFiles!!.isVisible = false
}
editor.apply()
}
@SuppressLint("BatteryLife", "InlinedApi")
private fun initListeners() {
val preferences =
PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = preferences.edit()
if(language!!.value == null) language!!.value = Locale.getDefault().language
language!!.summary = Locale(language!!.value).getDisplayLanguage(Locale(language!!.value))
language!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
language!!.summary = Locale(newValue.toString()).getDisplayLanguage(Locale(newValue.toString()))
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(newValue.toString()))
true
}
theme!!.summary = theme!!.entry
theme!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
when(newValue){
"System" -> {
theme!!.summary = getString(R.string.system)
}
"Dark" -> {
theme!!.summary = getString(R.string.dark)
}
else -> {
theme!!.summary = getString(R.string.light)
}
}
ThemeUtil.updateTheme(requireActivity() as AppCompatActivity)
val intent = Intent(requireContext(), MainActivity::class.java)
this.startActivity(intent)
requireActivity().finishAffinity()
true
}
accent!!.summary = accent!!.entry
accent!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, _: Any ->
ThemeUtil.updateTheme(requireActivity() as AppCompatActivity)
val intent = Intent(requireContext(), MainActivity::class.java)
this.startActivity(intent)
requireActivity().finishAffinity()
true
}
highContrast!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, _: Any ->
ThemeUtil.updateTheme(requireActivity() as AppCompatActivity)
val intent = Intent(requireContext(), MainActivity::class.java)
this.startActivity(intent)
requireActivity().finishAffinity()
true
}
ignoreBatteryOptimization!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent()
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:" + requireContext().packageName)
startActivity(intent)
true
}
musicPath!!.summary = fileUtil?.formatPath(preferences.getString("music_path", "")!!)
musicPath!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
musicPathResultLauncher.launch(intent)
true
}
videoPath!!.summary = fileUtil?.formatPath(preferences.getString("video_path", "")!!)
videoPath!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
videoPathResultLauncher.launch(intent)
true
}
commandPath!!.summary = fileUtil?.formatPath(preferences.getString("command_path", "")!!)
commandPath!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
commandPathResultLauncher.launch(intent)
true
}
accessAllFiles!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = Uri.parse("package:" + requireContext().packageName)
intent.data = uri
startActivity(intent)
true
}
var cacheSize = File(requireContext().cacheDir.absolutePath + "/downloads").walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
clearCache!!.summary = "(${fileUtil!!.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}"
clearCache!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
if (activeDownloadCount == 0){
File(requireContext().cacheDir.absolutePath + "/downloads").deleteRecursively()
Snackbar.make(requireView(), getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show()
cacheSize = File(requireContext().cacheDir.absolutePath + "/downloads").walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
clearCache!!.summary = "(${fileUtil!!.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}"
}else{
Snackbar.make(requireView(), getString(R.string.downloads_running_try_later), Snackbar.LENGTH_SHORT).show()
}
true
}
locale!!.summary = locale!!.value
concurrentDownloads!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, _: Any ->
MaterialAlertDialogBuilder(requireContext())
.setTitle(resources.getString(R.string.warning))
.setMessage(resources.getString(R.string.workmanager_updated))
.setNegativeButton(resources.getString(R.string.cancel)) { dialog, _ ->
dialog.cancel()
}
.setPositiveButton(resources.getString(R.string.restart)) { _, _ ->
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
requireContext().startActivity(intent)
if (context is Activity) {
(context as Activity).finish()
}
Runtime.getRuntime().exit(0)
}
.show()
true
}
audioFormat!!.summary = preferences.getString("audio_format", "")!!.replace("Default", getString(R.string.defaultValue))
audioFormat!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
audioFormat!!.summary = newValue.toString().replace("Default", getString(R.string.defaultValue))
true
}
videoFormat!!.summary = preferences.getString("video_format", "")!!.replace("Default", getString(R.string.defaultValue))
videoFormat!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
videoFormat!!.summary = newValue.toString().replace("Default", getString(R.string.defaultValue))
true
}
videoQuality!!.summary = preferences.getString("video_quality", "")
videoQuality!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
videoQuality!!.summary = newValue.toString()
true
}
formatID!!.summary = formatID!!.text
formatID!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
formatID!!.summary = newValue.toString()
true
}
YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it)
editor.apply()
ytdlVersion!!.summary = it
}
updateYTDL!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
lifecycleScope.launch {
Snackbar.make(requireView(),
requireContext().getString(R.string.ytdl_updating_started),
Snackbar.LENGTH_LONG).show()
when (updateUtil!!.updateYoutubeDL()) {
YoutubeDL.UpdateStatus.DONE -> {
Snackbar.make(requireView(),
requireContext().getString(R.string.ytld_update_success),
Snackbar.LENGTH_LONG).show()
YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it)
editor.apply()
ytdlVersion!!.summary = it
}
}
YoutubeDL.UpdateStatus.ALREADY_UP_TO_DATE -> Snackbar.make(requireView(),
requireContext().getString(R.string.you_are_in_latest_version),
Snackbar.LENGTH_LONG).show()
else -> {
Snackbar.make(requireView(),
requireContext().getString(R.string.errored),
Snackbar.LENGTH_LONG).show()
}
}
}
true
}
formatSource!!.summary = preferences.getString("formats_source", "")!!.replace("Default", getString(R.string.defaultValue))
formatSource!!.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
formatSource!!.summary = newValue.toString().replace("Default", getString(R.string.defaultValue))
true
}
exportPreferences!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val prefs = preferences.all
val json = buildJsonObject {
putJsonArray("YTDLnis_Preferences") {
prefs.forEach {
add(buildJsonObject {
put("key", it.key)
put("value", it.value.toString())
put("type", it.value!!::class.simpleName)
})
}
}
}
val string = jsonFormat.encodeToString(json)
val clipboard: ClipboardManager =
requireContext().getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setText(string)
Snackbar.make(requireView(), getString(R.string.copied_to_clipboard), Snackbar.LENGTH_LONG).show()
true
}
importPreferences!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val clipboard = requireContext().getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager
val clip = clipboard.primaryClip!!.getItemAt(0).text.toString()
try{
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit(commit = true) {
clear()
val preferencesKeys = preferences.all.map { it.key }
jsonFormat.decodeFromString<JsonObject>(clip).run {
val prefs = this.jsonObject["YTDLnis_Preferences"]!!.jsonArray
prefs.forEach {
val key : String = it.jsonObject["key"].toString().replace("\"", "")
if (preferencesKeys.contains(key)){
when(it.jsonObject["type"].toString().replace("\"", "")){
"String" -> {
val value = it.jsonObject["value"].toString().replace("\"", "")
putString(key, value)
}
"Boolean" -> {
val value = it.jsonObject["value"].toString().replace("\"", "").toBoolean()
putBoolean(key, value)
}
"Int" -> {
val value = it.jsonObject["value"].toString().replace("\"", "").toInt()
putInt(key, value)
}
"HashSet" -> {
val value = hashSetOf(it.jsonObject["value"].toString().replace("(\")|(\\[)|(])".toRegex(), ""))
putStringSet(key, value)
}
}
}
}
}
}
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
requireContext().startActivity(intent)
if (context is Activity) {
(context as Activity).finish()
}
Runtime.getRuntime().exit(0)
}catch (e: Exception){
e.printStackTrace()
}
true
}
version!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
lifecycleScope.launch{
withContext(Dispatchers.IO){
updateUtil!!.updateApp{ msg ->
lifecycleScope.launch(Dispatchers.Main){
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show()
}
}
}
}
true
}
}
private var musicPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(musicPath, result.data, MUSIC_PATH_CODE)
}
}
private var videoPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(videoPath, result.data, VIDEO_PATH_CODE)
}
}
private var commandPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(commandPath, result.data, COMMAND_PATH_CODE)
}
}
private fun changePath(p: Preference?, data: Intent?, requestCode: Int) {
val path = data!!.data.toString()
p!!.summary = fileUtil?.formatPath(data.data.toString())
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = sharedPreferences.edit()
when (requestCode) {
MUSIC_PATH_CODE -> editor.putString("music_path", path)
VIDEO_PATH_CODE -> editor.putString("video_path", path)
COMMAND_PATH_CODE -> editor.putString("command_path", path)
}
editor.apply()
}
companion object {
const val MUSIC_PATH_CODE = 33333
const val VIDEO_PATH_CODE = 55555
const val COMMAND_PATH_CODE = 77777
}
}

View file

@ -0,0 +1,89 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class UpdateSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.updating
private var updateYTDL: Preference? = null
private var ytdlVersion: Preference? = null
private var version: Preference? = null
private var updateUtil: UpdateUtil? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.updating_preferences, rootKey)
updateUtil = UpdateUtil(requireContext())
val preferences =
PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = preferences.edit()
updateYTDL = findPreference("update_ytdl")
ytdlVersion = findPreference("ytdl-version")
version = findPreference("version")
version!!.summary = BuildConfig.VERSION_NAME
YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it)
editor.apply()
ytdlVersion!!.summary = it
}
updateYTDL!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
lifecycleScope.launch {
Snackbar.make(requireView(),
requireContext().getString(R.string.ytdl_updating_started),
Snackbar.LENGTH_LONG).show()
when (updateUtil!!.updateYoutubeDL()) {
YoutubeDL.UpdateStatus.DONE -> {
Snackbar.make(requireView(),
requireContext().getString(R.string.ytld_update_success),
Snackbar.LENGTH_LONG).show()
YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it)
editor.apply()
ytdlVersion!!.summary = it
}
}
YoutubeDL.UpdateStatus.ALREADY_UP_TO_DATE -> Snackbar.make(requireView(),
requireContext().getString(R.string.you_are_in_latest_version),
Snackbar.LENGTH_LONG).show()
else -> {
Snackbar.make(requireView(),
requireContext().getString(R.string.errored),
Snackbar.LENGTH_LONG).show()
}
}
}
true
}
version!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
lifecycleScope.launch{
withContext(Dispatchers.IO){
updateUtil!!.updateApp{ msg ->
lifecycleScope.launch(Dispatchers.Main){
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show()
}
}
}
}
true
}
}
}

View file

@ -525,49 +525,63 @@ class InfoUtil(private val context: Context) {
urls.forEach {
urlsFile.appendText(it+"\n")
}
try {
val request = YoutubeDLRequest(emptyList())
request.addOption("--print", "%(formats)s")
request.addOption("-a", urlsFile.absolutePath)
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
val cookiesFile = File(context.cacheDir, "cookies.txt")
if (cookiesFile.exists()){
request.addOption("--cookies", cookiesFile.absolutePath)
}
YoutubeDL.getInstance().execute(request){ progress, _, line ->
try{
val formats = mutableListOf<Format>()
val listOfStrings = JSONArray(line)
for (f in 0 until listOfStrings.length()){
val format = listOfStrings.get(f) as JSONObject
try{
if (format.getString("filesize") == "None") continue
}catch (e: Exception) { continue }
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
if (formatProper.filesize > 0){
if ( !formatProper!!.format_note!!.contains("audio only", true)) {
formatProper.format_note = format.getString("format_note")
}else{
formatProper.format_note = "${format.getString("format_note")} audio"
}
formatProper.container = format.getString("ext")
formats.add(formatProper)
}
}
progress(formats)
}catch (e: Exception){
progress(emptyList())
val formatSource = sharedPreferences.getString("formats_source", "yt-dlp")
val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?")
val allYoutubeLinks = urls.any {p.matcher(it).find() }
if (formatSource == "yt-dlp" || !allYoutubeLinks){
try {
val request = YoutubeDLRequest(emptyList())
request.addOption("--print", "%(formats)s")
request.addOption("-a", urlsFile.absolutePath)
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
val cookiesFile = File(context.cacheDir, "cookies.txt")
if (cookiesFile.exists()){
request.addOption("--cookies", cookiesFile.absolutePath)
}
YoutubeDL.getInstance().execute(request){ progress, _, line ->
try{
val formats = mutableListOf<Format>()
val listOfStrings = JSONArray(line)
for (f in 0 until listOfStrings.length()){
val format = listOfStrings.get(f) as JSONObject
try{
if (format.getString("filesize") == "None") continue
}catch (e: Exception) { continue }
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
if (formatProper.filesize > 0){
if ( !formatProper!!.format_note!!.contains("audio only", true)) {
formatProper.format_note = format.getString("format_note")
}else{
formatProper.format_note = "${format.getString("format_note")} audio"
}
formatProper.container = format.getString("ext")
formats.add(formatProper)
}
}
progress(formats)
}catch (e: Exception){
progress(emptyList())
}
}
} catch (e: Exception) {
Looper.prepare().run {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
e.printStackTrace()
}
} catch (e: Exception) {
Looper.prepare().run {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}else{
urls.forEach {
val id = getIDFromYoutubeURL(it)
val res = genericRequest(invidousURL + "videos/" + id)
val vid = createVideoFromInvidiousJSON(res)
progress(vid!!.formats)
}
e.printStackTrace()
}
urlsFile.delete()
}
@ -741,6 +755,7 @@ class InfoUtil(private val context: Context) {
val url = "https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&client=firefox&q=$query"
// invidousURL + "search/suggestions?q=" + query
val res = genericArrayRequest(url)
Log.e("aa", res.toString())
if (res.length() == 0) return ArrayList()
val suggestionList = ArrayList<String>()
try {

View file

@ -303,6 +303,107 @@ class UiUtil(private val fileUtil: FileUtil) {
datePicker.show(fragmentManager, "datepicker")
}
fun showFormatDetails(format: Format, activity: Activity){
val bottomSheet = BottomSheetDialog(activity)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.format_details_sheet)
val formatIdParent = bottomSheet.findViewById<LinearLayout>(R.id.format_id_parent)
val containerParent = bottomSheet.findViewById<LinearLayout>(R.id.container_parent)
val codecParent = bottomSheet.findViewById<LinearLayout>(R.id.codec_parent)
val filesizeParent = bottomSheet.findViewById<LinearLayout>(R.id.filesize_parent)
val formatnoteParent = bottomSheet.findViewById<LinearLayout>(R.id.format_note_parent)
val fpsParent = bottomSheet.findViewById<LinearLayout>(R.id.fps_parent)
val asrParent = bottomSheet.findViewById<LinearLayout>(R.id.asr_parent)
if (format.format_id.isBlank()) formatIdParent?.visibility = View.GONE
else {
formatIdParent?.findViewById<TextView>(R.id.format_id_value)?.text = format.format_id
formatIdParent?.setOnClickListener {
copyToClipboard(format.format_id, activity)
}
}
if (format.container.isBlank()) containerParent?.visibility = View.GONE
else {
containerParent?.findViewById<TextView>(R.id.container_value)?.text = format.container
containerParent?.setOnClickListener {
copyToClipboard(format.container, activity)
}
}
val codecField =
if (format.encoding != "") {
format.encoding.uppercase()
}else if (format.vcodec != "none" && format.vcodec != ""){
format.vcodec.uppercase()
} else {
format.acodec.uppercase()
}
if (codecField.isBlank()) codecParent?.visibility = View.GONE
else {
codecParent?.findViewById<TextView>(R.id.codec_value)?.text = codecField
codecParent?.setOnClickListener {
copyToClipboard(codecField, activity)
}
}
if (format.filesize != 0L) filesizeParent?.visibility = View.GONE
else {
filesizeParent?.findViewById<TextView>(R.id.filesize_value)?.text = fileUtil.convertFileSize(format.filesize)
filesizeParent?.setOnClickListener {
copyToClipboard(fileUtil.convertFileSize(format.filesize), activity)
}
}
if (format.format_note.isBlank()) formatnoteParent?.visibility = View.GONE
else {
formatnoteParent?.findViewById<TextView>(R.id.format_note_value)?.text = format.format_note
formatnoteParent?.setOnClickListener {
copyToClipboard(format.format_note, activity)
}
}
if (format.fps.isNullOrBlank()) fpsParent?.visibility = View.GONE
else {
fpsParent?.findViewById<TextView>(R.id.fps_value)?.text = format.fps
fpsParent?.setOnClickListener {
copyToClipboard(format.fps!!, activity)
}
}
if (format.asr.isNullOrBlank()) asrParent?.visibility = View.GONE
else {
asrParent?.findViewById<TextView>(R.id.asr_value)?.text = format.asr
asrParent?.setOnClickListener {
copyToClipboard(format.asr!!, activity)
}
}
bottomSheet.show()
val displayMetrics = DisplayMetrics()
activity.windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
private fun copyToClipboard(text: String, activity: Activity){
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(text, text)
clipboard.setPrimaryClip(clip)
Toast.makeText(activity, activity.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT)
.show()
}
suspend fun showCommandTemplates(activity: Activity, commandTemplateViewModel: CommandTemplateViewModel, itemSelected: (itemSelected: CommandTemplate) -> Unit) {
val bottomSheet = BottomSheetDialog(activity)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)

View file

@ -47,7 +47,7 @@ class UpdateUtil(var context: Context) {
version = res.getString("tag_name")
body = res.getString("body")
} catch (e: JSONException) {
result(e.message.toString())
result(context.getString(R.string.network_error))
return
}
val versionNameInt = version.split("v")[1].replace(".","").toInt()

View file

@ -178,8 +178,8 @@ class DownloadWorker(
when(type){
DownloadViewModel.Type.audio -> {
var audioQualityId : String = downloadItem.format.format_id
if (audioQualityId.isBlank() || audioQualityId == "0" || audioQualityId == context.getString(R.string.best_quality)) audioQualityId = ""
else if (audioQualityId == context.getString(R.string.worst_quality)) audioQualityId = "worstaudio"
if (audioQualityId.isBlank() || audioQualityId == "0" || audioQualityId == context.getString(R.string.best_quality) || audioQualityId == "best") audioQualityId = ""
else if (audioQualityId == context.getString(R.string.worst_quality) || audioQualityId == "worst") audioQualityId = "worstaudio"
val ext = downloadItem.format.container
if (audioQualityId.isNotBlank()) request.addOption("-f", audioQualityId)
@ -236,15 +236,15 @@ class DownloadWorker(
Log.e(TAG, videoFormatID)
var formatArgument = if (downloadItem.videoPreferences.removeAudio) "bestvideo" else "bestvideo+bestaudio/best"
if (videoFormatID.isNotEmpty()) {
if (videoFormatID == context.resources.getString(R.string.best_quality)) videoFormatID = "bestvideo"
else if (videoFormatID == context.resources.getString(R.string.worst_quality)) videoFormatID = "worst"
if (videoFormatID == context.resources.getString(R.string.best_quality) || videoFormatID == "best") videoFormatID = "bestvideo"
else if (videoFormatID == context.resources.getString(R.string.worst_quality) || videoFormatID == "worst") videoFormatID = "worst"
else if (defaultFormats.contains(videoFormatID)) videoFormatID = "bestvideo[height<="+videoFormatID.substring(0, videoFormatID.length -1)+"]"
if (!downloadItem.videoPreferences.removeAudio) formatArgument = "$videoFormatID+bestaudio/best/$videoFormatID"
}
Log.e(TAG, formatArgument)
request.addOption("-f", formatArgument)
val format = downloadItem.format.container
if(format.isNotEmpty()){
if(format.isNotEmpty() && format != "Default" && format != context.getString(R.string.defaultValue)){
request.addOption("--merge-output-format", format)
if (format != "webm") {
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)

View file

@ -11,7 +11,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Incognito"
android:text="@string/incognito"
android:textStyle="bold"
android:translationZ="20dp"
android:visibility="gone"

View file

@ -11,7 +11,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Incognito"
android:text="@string/incognito"
android:textStyle="bold"
android:translationZ="20dp"
android:visibility="gone"

View file

@ -29,6 +29,7 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/logs_recyclerview"
android:layout_width="match_parent"
android:scrollbars="vertical"
android:layout_height="match_parent"
android:clipToPadding="false"
>

View file

@ -11,7 +11,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Incognito"
android:text="@string/incognito"
android:textStyle="bold"
android:visibility="gone"
android:padding="5dp"

View file

@ -6,32 +6,44 @@
android:layout_height="match_parent"
tools:context="com.deniscerri.ytdlnis.ui.more.settings.SettingsActivity">
<FrameLayout
android:id="@+id/settings_frame_layout"
android:layout_width="match_parent"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/frame_layout"
android:name="androidx.navigation.fragment.NavHostFragment"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/settings_nav"
app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@+id/incognito_header"
/>
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:liftOnScroll="true"
android:background="@android:color/transparent"
android:fitsSystemWindows="true">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/settings_toolbar"
android:elevation="0dp"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:title="@string/settings"
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent"
app:navigationIcon="@drawable/ic_back"
android:layout_height="match_parent"/>
app:title="@string/settings"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/settings_toolbar"
android:elevation="0dp"
app:layout_collapseMode="pin"
app:layout_scrollFlags="scroll"
app:navigationIcon="@drawable/ic_back"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>

View file

@ -1,154 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.core.widget.NestedScrollView android:layout_width="match_parent"
android:scrollbars="none"
android:orientation="vertical"
android:layout_height="wrap_content"
android:fillViewport="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.core.widget.NestedScrollView
<LinearLayout
android:layout_width="match_parent"
android:scrollbars="none"
android:orientation="vertical"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:paddingTop="20dp">
<TextView
android:id="@+id/bottom_sheet_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:maxLines="2"
android:singleLine="false"
android:text="@string/download"
android:textSize="25sp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/bottom_sheet_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxLines="2"
android:singleLine="false"
android:text="@string/configure_download"
android:textSize="12sp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_title" />
<Button
android:id="@+id/bottomsheet_schedule_button"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
app:icon="@drawable/ic_clock"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_download_button"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/bottomsheet_download_button"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/download"
app:icon="@drawable/ic_down"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/download_tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:backgroundTint="@android:color/transparent"
app:tabGravity="start"
app:tabMode="scrollable">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/audio" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/video" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/command" />
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/download_viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/bottom_sheet_link"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textAlignment="textStart"
android:singleLine="true"
android:text="@string/app_name"
android:textSize="15sp"
app:icon="@drawable/ic_link" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:paddingTop="20dp">
<TextView
android:id="@+id/bottom_sheet_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:maxLines="2"
android:singleLine="false"
android:text="@string/download"
android:textSize="25sp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/bottom_sheet_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxLines="2"
android:singleLine="false"
android:text="@string/configure_download"
android:textSize="12sp"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_schedule_button"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_title" />
<Button
android:id="@+id/bottomsheet_schedule_button"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
app:icon="@drawable/ic_clock"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_download_button"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/bottomsheet_download_button"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/download"
app:icon="@drawable/ic_down"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/download_tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:backgroundTint="@android:color/transparent"
app:tabGravity="fill"
app:tabMode="scrollable">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/audio" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/video" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/command" />
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/download_viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:gravity="end"
android:visibility="gone"
android:paddingBottom="10dp"
android:paddingHorizontal="20dp"
android:layout_height="wrap_content">
<Button
android:id="@+id/bottom_sheet_link"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:id="@+id/update_item"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textAlignment="textStart"
android:singleLine="true"
android:text="@string/app_name"
android:textSize="15sp"
app:icon="@drawable/ic_link" />
<LinearLayout
android:layout_width="match_parent"
android:gravity="end"
android:visibility="gone"
android:paddingBottom="10dp"
android:paddingHorizontal="20dp"
android:layout_height="wrap_content">
<Button
android:id="@+id/update_item"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/update"
app:icon="@drawable/ic_update"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
android:autoLink="all"
android:text="@string/update"
app:icon="@drawable/ic_update"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -6,6 +6,7 @@
android:checkable="true"
android:clickable="true"
android:focusable="true"
app:checkedIcon="@null"
app:cardPreventCornerOverlap="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -156,6 +156,21 @@
android:checked="false"
android:text="@string/split_by_chapters"/>
<com.google.android.material.chip.Chip
android:id="@+id/cut"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_cut"
android:text="@string/cut"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
@ -180,7 +195,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="SponsorBlock"
android:text="@string/sponsorblock"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_money"
android:minWidth="30dp"
@ -190,14 +205,14 @@
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/cut"
android:id="@+id/filename_template"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/file_name_template"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_cut"
android:text="@string/cut"
app:chipIcon="@drawable/ic_edit"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
@ -205,7 +220,6 @@
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>

View file

@ -33,7 +33,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="10dp">
android:paddingHorizontal="10dp"
android:baselineAligned="false">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/author_textinput"
@ -57,8 +58,10 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="45"
android:labelFor="@id/container_textview"
android:hint="@string/container"
android:paddingStart="10dp">
android:paddingStart="10dp"
android:paddingEnd="0dp">
<AutoCompleteTextView
android:id="@+id/container_textview"
@ -175,14 +178,12 @@
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/sponsorblock_filters"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:id="@+id/remove_audio"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="SponsorBlock"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_money"
android:text="@string/remove_audio"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
@ -227,6 +228,36 @@
android:layout_height="wrap_content"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/sponsorblock_filters"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/sponsorblock"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_money"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/filename_template"
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/file_name_template"
app:chipIconVisible="true"
app:chipIcon="@drawable/ic_edit"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/save_subtitles"
style="@style/Widget.Material3.Chip.Filter.Elevated"
@ -249,19 +280,6 @@
android:checked="false"
android:text="@string/subtitle_languages"/>
<com.google.android.material.chip.Chip
android:id="@+id/remove_audio"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/remove_audio"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>

View file

@ -10,6 +10,7 @@
app:liftOnScroll="true"
android:background="@android:color/transparent"
android:elevation="0dp"
app:liftOnScrollTargetViewId="@id/recyclerviewhistorys"
app:elevation="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">

View file

@ -21,6 +21,7 @@
android:layout_height="match_parent"
app:cardCornerRadius="10dp"
app:cardElevation="5dp"
app:checkedIcon="@null"
app:cardMaxElevation="12dp"
android:checkable="true"
app:strokeWidth="0dp"

View file

@ -20,6 +20,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="20dp"
app:checkedIcon="@null"
app:cardElevation="5dp"
app:cardMaxElevation="12dp"
app:cardPreventCornerOverlap="true"

View file

@ -20,23 +20,26 @@
style="@style/Widget.Material3.BottomAppBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/select_playlist_menu"
android:layout_gravity="bottom">
<TextView
android:id="@+id/selected"
android:layout_width="wrap_content"
android:textStyle="bold"
android:layout_marginHorizontal="30dp"
android:layout_height="wrap_content" />
</com.google.android.material.bottomappbar.BottomAppBar>
<com.google.android.material.floatingactionbutton.FloatingActionButton
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/check_all"
android:layout_width="wrap_content"
android:text="@string/select_all"
app:icon="@drawable/ic_select_all"
android:layout_height="wrap_content"
app:layout_anchor="@id/bottomAppBar"
app:srcCompat="@drawable/ic_select_all" />
app:layout_anchor="@id/bottomAppBar"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@ -83,6 +86,7 @@
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -10,7 +10,6 @@
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="URL"
android:padding="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:theme="@style/BaseTheme">
<item android:title="@string/invert_selected"
android:icon="@drawable/ic_terminal"
app:showAsAction="never"
android:id="@+id/invert_selected"/>
</menu>

View file

@ -26,13 +26,21 @@
android:label="More"
tools:layout="@layout/fragment_more" >
<action
android:id="@+id/action_moreFragment_to_settingsFragment"
app:destination="@id/settingsFragment" />
android:id="@+id/action_moreFragment_to_terminalActivity"
app:destination="@id/terminalActivity" />
<action
android:id="@+id/action_moreFragment_to_cookiesActivity"
app:destination="@id/cookiesActivity" />
<action
android:id="@+id/action_moreFragment_to_downloadQueueActivity"
app:destination="@id/downloadQueueActivity" />
<action
android:id="@+id/action_moreFragment_to_downloadLogListActivity"
app:destination="@id/downloadLogListActivity" />
<action
android:id="@+id/action_moreFragment_to_commandTemplatesActivity"
app:destination="@id/commandTemplatesActivity" />
</fragment>
<fragment
android:id="@+id/settingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.SettingsFragment"
android:label="SettingsFragment" />
<activity
android:id="@+id/downloadQueueActivity"
android:name="com.deniscerri.ytdlnis.ui.downloads.DownloadQueueActivity"
@ -50,11 +58,6 @@
android:id="@+id/cookiesActivity"
android:name="com.deniscerri.ytdlnis.ui.more.CookiesActivity"
android:label="CookiesActivity" />
<activity
android:id="@+id/settingsActivity"
android:name="com.deniscerri.ytdlnis.ui.more.settings.SettingsActivity"
android:label="activity_settings"
tools:layout="@layout/activity_settings" />
<activity
android:id="@+id/terminalActivity"
android:name="com.deniscerri.ytdlnis.ui.more.TerminalActivity"

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/settings_nav_graph"
app:startDestination="@id/mainSettingsFragment">
<fragment
android:id="@+id/appearanceSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.AppearanceSettingsFragment"
android:label="AppearanceSettingsFragment" />
<fragment
android:id="@+id/processingSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.ProcessingSettingsFragment"
android:label="ProcessingSettingsFragment" />
<fragment
android:id="@+id/folderSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.FolderSettingsFragment"
android:label="FolderSettingsFragment" />
<fragment
android:id="@+id/updateSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.UpdateSettingsFragment"
android:label="UpdateSettingsFragment" />
<fragment
android:id="@+id/downloadSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.DownloadSettingsFragment"
android:label="DownloadSettingsFragment" />
<fragment
android:id="@+id/mainSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.MainSettingsFragment"
android:label="MainSettingsFragment" >
<action
android:id="@+id/action_mainSettingsFragment_to_appearanceSettingsFragment"
app:destination="@id/appearanceSettingsFragment" />
<action
android:id="@+id/action_mainSettingsFragment_to_folderSettingsFragment"
app:destination="@id/folderSettingsFragment" />
<action
android:id="@+id/action_mainSettingsFragment_to_downloadSettingsFragment"
app:destination="@id/downloadSettingsFragment" />
<action
android:id="@+id/action_mainSettingsFragment_to_processingSettingsFragment"
app:destination="@id/processingSettingsFragment" />
<action
android:id="@+id/action_mainSettingsFragment_to_updateSettingsFragment"
app:destination="@id/updateSettingsFragment" />
</fragment>
</navigation>

View file

@ -11,7 +11,7 @@
</string-array>
<string-array name="audio_containers_values">
<item />
<item>Default</item>
<item>mp3</item>
<item>m4a</item>
<item>aac</item>
@ -29,7 +29,7 @@
</string-array>
<string-array name="video_containers_values">
<item />
<item>Default</item>
<item>mp4</item>
<item>mkv</item>
<item>webm</item>
@ -46,6 +46,17 @@
<item>@string/best_quality</item>
</string-array>
<string-array name="video_formats_values">
<item>worst</item>
<item>360p</item>
<item>480p</item>
<item>720p</item>
<item>1080p</item>
<item>1440p</item>
<item>2160p</item>
<item>best</item>
</string-array>
<string-array name="audio_formats">
<item>@string/worst_quality</item>
<item>@string/best_quality</item>
@ -683,4 +694,28 @@
<item>ZW</item>
</array>
<array name="backup_category_entries">
<item>@string/settings</item>
<item>@string/downloads</item>
<item>@string/in_queue</item>
<item>@string/cancelled</item>
<item>@string/cookies</item>
<item>@string/command_templates</item>
<item>@string/shortcuts</item>
<item>@string/search_history</item>
</array>
<array name="backup_category_values">
<item>settings</item>
<item>downloads</item>
<item>queued</item>
<item>cancelled</item>
<item>cookies</item>
<item>templates</item>
<item>shortcuts</item>
<item>searchHistory</item>
</array>
</resources>

View file

@ -255,4 +255,13 @@
<string name="keep_cache_summary">Don\'t delete temporary files after the download is finished</string>
<string name="backup">Backup</string>
<string name="restore">Restore</string>
<string name="network_error">Network Error</string>
<string name="incognito">Incognito</string>
<string name="sponsorblock">SponsorBlock</string>
<string name="couldnt_parse_file">Couldn\'t parse file</string>
<string name="restore_complete">App Restore Complete. The following items have been restored:</string>
<string name="select_backup_categories">Select backup categories</string>
<string name="search_history">Search History</string>
<string name="backup_created_successfully">Backup file saved in the downloads folder</string>
<string name="appearance">Appearance</string>
</resources>

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
android:entries="@array/language_values"
android:entryValues="@array/language_values"
android:icon="@drawable/ic_language"
app:key="app_language"
app:summary="en"
app:title="@string/language" />
<ListPreference
android:entries="@array/themes"
android:entryValues="@array/themes_values"
android:icon="@drawable/baseline_format_paint_24"
app:defaultValue="System"
app:key="ytdlnis_theme"
app:summary="@string/defaultValue"
app:title="@string/Theme" />
<ListPreference
android:entries="@array/accents"
android:entryValues="@array/accents_values"
android:icon="@drawable/baseline_color_lens_24"
app:defaultValue="blue"
app:key="theme_accent"
app:summary="Blue"
app:title="@string/accents" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_invert_colors_24"
android:key="high_contrast"
app:summary="@string/pure_theme_summary"
app:title="@string/high_contrast" />
</PreferenceScreen>

View file

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_incognito"
android:key="incognito"
app:summary="@string/incognito_summary"
app:title="@string/incognito" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:icon="@drawable/ic_card"
android:key="download_card"
app:summary="@string/download_card_summary"
app:title="@string/show_download_card" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_speed"
android:key="quick_download"
app:summary="@string/quick_download_summary"
app:title="@string/quick_download" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:icon="@drawable/metered_networks"
android:key="metered_networks"
app:summary="@string/download_over_metered_networks_summary"
app:title="@string/download_over_metered_networks" />
<ListPreference
android:dependency="download_card"
android:defaultValue="video"
android:entries="@array/download_types"
android:entryValues="@array/download_types_values"
android:icon="@drawable/ic_download_type"
app:key="preferred_download_type"
app:summary="@string/preferred_download_type_summary"
app:title="@string/preferred_download_type" />
<ListPreference
android:defaultValue="ytsearch"
android:entries="@array/search_engines"
android:entryValues="@array/search_engines_values"
android:icon="@drawable/baseline_manage_search_24"
app:key="search_engine"
app:summary="@string/preferred_search_engine_summary"
app:title="@string/preferred_search_engine" />
<ListPreference
android:defaultValue="Home"
android:entries="@array/start_destination"
android:entryValues="@array/start_destination_values"
app:icon="@drawable/baseline_add_to_home_screen_24"
app:key="start_destination"
app:title="@string/preferred_home_screen" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_recommend_24"
android:key="home_recommendations"
app:summary="@string/video_recommendations_summary"
app:title="@string/video_recommendations" />
<ListPreference
android:defaultValue=""
android:entries="@array/countries"
android:entryValues="@array/countries_values"
app:icon="@drawable/ic_language"
app:dependency="home_recommendations"
app:key="locale"
app:title="@string/preferred_locale" />
<EditTextPreference
android:icon="@drawable/ic_key"
app:key="api_key"
app:defaultValue=""
app:summary="@string/api_key_summary"
app:title="@string/api_key" />
<SeekBarPreference
app:dependency="aria2"
android:defaultValue="3"
android:icon="@drawable/ic_lines"
android:max="25"
app:key="concurrent_fragments"
app:min="1"
app:showSeekBarValue="true"
android:dependency="aria2"
app:summary="@string/concurrent_fragments_summary"
app:title="@string/concurrent_fragments" />
<SeekBarPreference
android:defaultValue="1"
android:icon="@drawable/ic_concurrent_downloads"
android:max="10"
app:key="concurrent_downloads"
app:min="1"
app:showSeekBarValue="true"
app:summary="@string/concurrent_downloads_summary"
app:title="@string/concurrent_downloads" />
<EditTextPreference
android:icon="@drawable/ic_down"
app:key="limit_rate"
app:summary="@string/limit_rate_summary"
app:title="@string/limit_rate" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_speed"
android:key="aria2"
app:summary="@string/aria2_summary"
android:disableDependentsState="true"
app:title="Aria2" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_baseline_file_open_24"
android:key="log_downloads"
app:summary="@string/log_downloads_summary"
app:title="@string/log_downloads" />
<Preference
app:icon="@drawable/ic_battery"
app:key="ignore_battery"
app:title="@string/ignore_battery_optimization" />
</PreferenceScreen>

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:icon="@drawable/ic_music_downloaded"
app:key="music_path"
app:defaultValue="@string/music_path"
app:title="@string/music_directory" />
<Preference
app:icon="@drawable/ic_video_downloaded"
app:key="video_path"
app:defaultValue="@string/video_path"
app:title="@string/video_directory" />
<Preference
app:icon="@drawable/ic_terminal"
app:key="command_path"
app:defaultValue="@string/command_path"
app:title="@string/command_directory" />
<Preference
app:icon="@drawable/ic_access_folder"
app:key="access_all_files"
android:summary="@string/access_all_directories_summary"
app:title="@string/access_all_directories" />
<Preference
app:icon="@drawable/ic_folder_delete"
app:key="clear_cache"
android:summary="@string/clear_temporary_files_summary"
app:title="@string/clear_temporary_files" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_save_24"
android:key="keep_cache"
app:summary="@string/keep_cache_summary"
app:title="@string/keep_cache" />
</PreferenceScreen>

View file

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<MultiSelectListPreference
app:defaultValue="@array/sponsorblock_settings_default"
app:icon="@drawable/ic_money"
app:dialogTitle="@string/select_sponsorblock_filtering"
app:entries="@array/sponsorblock_settings_entries"
app:entryValues="@array/sponsorblock_settings_values"
app:key="sponsorblock_filters"
app:summary="@string/select_sponsorblock_filtering"
app:title="SponsorBlock" />
<EditTextPreference
android:icon="@drawable/ic_textformat"
app:key="file_name_template"
app:useSimpleSummaryProvider="true"
app:defaultValue="%(uploader)s - %(title)s"
app:title="@string/file_name_template" />
<EditTextPreference
android:icon="@drawable/ic_textformat"
app:key="file_name_template_audio"
app:useSimpleSummaryProvider="true"
app:defaultValue="%(uploader)s - %(title)s"
app:title="@string/file_name_template" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/if_file_rename"
app:key="restrict_filenames"
app:summary="@string/restrict_filenames_summary"
app:title="@string/restrict_filenames" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_clock"
app:key="mtime"
app:summary="@string/enable_mtime_summary"
app:title="@string/enable_mtime" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_subtitles"
app:key="embed_subtitles"
app:summary="@string/embed_subs_summary"
app:title="@string/embed_subtitles" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_subtitles_alternative"
app:key="write_subtitles"
app:summary="@string/save_subs_desc"
app:title="@string/save_subs" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_image"
app:key="embed_thumbnail"
app:summary="@string/embed_thumb_summary"
app:title="@string/embed_thumb" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_chapters"
app:key="add_chapters"
app:summary="@string/add_chapters_summary"
app:title="@string/add_chapters" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_image"
app:key="write_thumbnail"
app:summary="@string/save_thumb_summary"
app:title="@string/save_thumb" />
<ListPreference
android:defaultValue="Default"
android:entries="@array/audio_containers"
android:entryValues="@array/audio_containers_values"
android:icon="@drawable/ic_code"
app:key="audio_format"
app:useSimpleSummaryProvider="true"
app:title="@string/audio_format" />
<ListPreference
android:defaultValue="Default"
android:entries="@array/video_containers"
android:entryValues="@array/video_containers_values"
android:icon="@drawable/ic_code"
app:key="video_format"
app:useSimpleSummaryProvider="true"
app:title="@string/video_format" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_music"
android:max="10"
app:key="audio_quality"
app:min="0"
app:showSeekBarValue="true"
app:summary="@string/audio_quality_summary"
app:title="@string/audio_quality" />
<ListPreference
android:defaultValue="best"
android:entries="@array/video_formats"
android:entryValues="@array/video_formats_values"
android:icon="@drawable/ic_video"
app:key="video_quality"
app:useSimpleSummaryProvider="true"
app:title="@string/video_quality" />
<EditTextPreference
app:key="format_id"
app:defaultValue=""
app:useSimpleSummaryProvider="true"
app:title="@string/preferred_format_id" />
</PreferenceScreen>

View file

@ -1,398 +1,44 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
android:entries="@array/language_values"
android:entryValues="@array/language_values"
android:icon="@drawable/ic_language"
app:key="app_language"
app:summary="en"
app:title="@string/language" />
<ListPreference
android:entries="@array/themes"
android:entryValues="@array/themes_values"
android:icon="@drawable/baseline_format_paint_24"
app:defaultValue="System"
app:key="ytdlnis_theme"
app:summary="@string/defaultValue"
app:title="@string/Theme" />
<ListPreference
android:entries="@array/accents"
android:entryValues="@array/accents_values"
android:icon="@drawable/baseline_color_lens_24"
app:defaultValue="blue"
app:key="theme_accent"
app:summary="Blue"
app:title="@string/accents" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_invert_colors_24"
android:key="high_contrast"
app:summary="@string/pure_theme_summary"
app:title="@string/high_contrast" />
<Preference
app:icon="@drawable/ic_battery"
app:key="ignore_battery"
app:title="@string/ignore_battery_optimization" />
android:key="appearance"
app:icon="@drawable/baseline_format_paint_24"
android:title="@string/appearance"/>
<PreferenceCategory android:title="@string/directories">
<Preference
android:key="folders"
app:icon="@drawable/baseline_folder_24"
android:title="@string/directories"/>
<Preference
android:key="downloading"
app:icon="@drawable/baseline_download_24"
android:title="@string/downloading"/>
<Preference
app:icon="@drawable/ic_music_downloaded"
app:key="music_path"
app:defaultValue="@string/music_path"
app:title="@string/music_directory" />
<Preference
android:key="processing"
app:icon="@drawable/ic_code"
android:title="@string/processing"/>
<Preference
app:icon="@drawable/ic_video_downloaded"
app:key="video_path"
app:defaultValue="@string/video_path"
app:title="@string/video_directory" />
<Preference
app:icon="@drawable/ic_terminal"
app:key="command_path"
app:defaultValue="@string/command_path"
app:title="@string/command_directory" />
<Preference
app:icon="@drawable/ic_access_folder"
app:key="access_all_files"
android:summary="@string/access_all_directories_summary"
app:title="@string/access_all_directories" />
<Preference
app:icon="@drawable/ic_folder_delete"
app:key="clear_cache"
android:summary="@string/clear_temporary_files_summary"
app:title="@string/clear_temporary_files" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_save_24"
android:key="keep_cache"
app:summary="@string/keep_cache_summary"
app:title="@string/keep_cache" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/downloading">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_incognito"
android:key="incognito"
app:summary="@string/incognito_summary"
app:title="Incognito" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:icon="@drawable/ic_card"
android:key="download_card"
app:summary="@string/download_card_summary"
app:title="@string/show_download_card" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_speed"
android:key="quick_download"
app:summary="@string/quick_download_summary"
app:title="@string/quick_download" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:icon="@drawable/metered_networks"
android:key="metered_networks"
app:summary="@string/download_over_metered_networks_summary"
app:title="@string/download_over_metered_networks" />
<ListPreference
android:dependency="download_card"
android:defaultValue="video"
android:entries="@array/download_types"
android:entryValues="@array/download_types_values"
android:icon="@drawable/ic_download_type"
app:key="preferred_download_type"
app:summary="@string/preferred_download_type_summary"
app:title="@string/preferred_download_type" />
<ListPreference
android:defaultValue="ytsearch"
android:entries="@array/search_engines"
android:entryValues="@array/search_engines_values"
android:icon="@drawable/baseline_manage_search_24"
app:key="search_engine"
app:summary="@string/preferred_search_engine_summary"
app:title="@string/preferred_search_engine" />
<ListPreference
android:defaultValue="Home"
android:entries="@array/start_destination"
android:entryValues="@array/start_destination_values"
app:icon="@drawable/baseline_add_to_home_screen_24"
app:key="start_destination"
app:title="@string/preferred_home_screen" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_recommend_24"
android:key="home_recommendations"
app:summary="@string/video_recommendations_summary"
app:title="@string/video_recommendations" />
<ListPreference
android:defaultValue=""
android:entries="@array/countries"
android:entryValues="@array/countries_values"
app:icon="@drawable/ic_language"
app:dependency="home_recommendations"
app:key="locale"
app:title="@string/preferred_locale" />
<EditTextPreference
android:icon="@drawable/ic_key"
app:key="api_key"
app:defaultValue=""
app:summary="@string/api_key_summary"
app:title="@string/api_key" />
<SeekBarPreference
app:dependency="aria2"
android:defaultValue="3"
android:icon="@drawable/ic_lines"
android:max="25"
app:key="concurrent_fragments"
app:min="1"
app:showSeekBarValue="true"
android:dependency="aria2"
app:summary="@string/concurrent_fragments_summary"
app:title="@string/concurrent_fragments" />
<SeekBarPreference
android:defaultValue="1"
android:icon="@drawable/ic_concurrent_downloads"
android:max="10"
app:key="concurrent_downloads"
app:min="1"
app:showSeekBarValue="true"
app:summary="@string/concurrent_downloads_summary"
app:title="@string/concurrent_downloads" />
<EditTextPreference
android:icon="@drawable/ic_down"
app:key="limit_rate"
app:summary="@string/limit_rate_summary"
app:title="@string/limit_rate" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_speed"
android:key="aria2"
app:summary="@string/aria2_summary"
android:disableDependentsState="true"
app:title="Aria2" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_baseline_file_open_24"
android:key="log_downloads"
app:summary="@string/log_downloads_summary"
app:title="@string/log_downloads" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/processing">
<MultiSelectListPreference
app:defaultValue="@array/sponsorblock_settings_default"
app:icon="@drawable/ic_money"
app:dialogTitle="@string/select_sponsorblock_filtering"
app:entries="@array/sponsorblock_settings_entries"
app:entryValues="@array/sponsorblock_settings_values"
app:key="sponsorblock_filters"
app:summary="@string/select_sponsorblock_filtering"
app:title="SponsorBlock" />
<EditTextPreference
android:icon="@drawable/ic_textformat"
app:key="file_name_template"
app:defaultValue="%(uploader)s - %(title)s"
app:title="@string/file_name_template" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/if_file_rename"
app:key="restrict_filenames"
app:summary="@string/restrict_filenames_summary"
app:title="@string/restrict_filenames" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_clock"
app:key="mtime"
app:summary="@string/enable_mtime_summary"
app:title="@string/enable_mtime" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_subtitles"
app:key="embed_subtitles"
app:summary="@string/embed_subs_summary"
app:title="@string/embed_subtitles" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_subtitles_alternative"
app:key="write_subtitles"
app:summary="@string/save_subs_desc"
app:title="@string/save_subs" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_image"
app:key="embed_thumbnail"
app:summary="@string/embed_thumb_summary"
app:title="@string/embed_thumb" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_chapters"
app:key="add_chapters"
app:summary="@string/add_chapters_summary"
app:title="@string/add_chapters" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_image"
app:key="write_thumbnail"
app:summary="@string/save_thumb_summary"
app:title="@string/save_thumb" />
<ListPreference
android:defaultValue="Default"
android:entries="@array/audio_containers"
android:entryValues="@array/audio_containers_values"
android:icon="@drawable/ic_code"
app:key="audio_format"
app:summary="@string/defaultValue"
app:title="@string/audio_format" />
<ListPreference
android:defaultValue="Default"
android:entries="@array/video_containers"
android:entryValues="@array/video_containers_values"
android:icon="@drawable/ic_code"
app:key="video_format"
app:summary="@string/defaultValue"
app:title="@string/video_format" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_music"
android:max="10"
app:key="audio_quality"
app:min="0"
app:showSeekBarValue="true"
app:summary="@string/audio_quality_summary"
app:title="@string/audio_quality" />
<ListPreference
android:defaultValue="@string/best_quality"
android:entries="@array/video_formats"
android:entryValues="@array/video_formats"
android:icon="@drawable/ic_video"
app:key="video_quality"
app:summary="@string/best_quality"
app:title="@string/video_quality" />
<EditTextPreference
app:key="format_id"
app:defaultValue=""
app:title="@string/preferred_format_id" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/updating">
<Preference
app:icon="@drawable/ic_update"
app:key="update_ytdl"
app:summary="@string/ytdl_update_hint"
app:title="@string/update_ytdl" />
<Preference
app:icon="@drawable/ic_info"
app:key="ytdl-version"
app:title="@string/ytdl_version"/>
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
android:defaultValue="false"
app:icon="@drawable/ic_nightly"
app:key="nightly_ytdl"
android:title="@string/update_ytdl_nightly"
/>
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_format"
android:key="update_formats"
app:summary="@string/update_formats_summary"
app:title="@string/update_formats" />
<ListPreference
android:defaultValue="yt-dlp"
android:entries="@array/formats_source"
android:entryValues="@array/formats_source_values"
android:icon="@drawable/baseline_manage_search_24"
app:key="formats_source"
app:summary="yt-dlp"
app:title="@string/format_source" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:icon="@drawable/ic_update_app"
android:key="update_app"
app:summary="@string/update_app_summary"
app:title="@string/update_app" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/about">
<Preference
android:key="updating"
app:icon="@drawable/ic_update"
android:title="@string/updating"/>
<PreferenceCategory android:title="@string/backup">
<Preference
app:icon="@drawable/baseline_save_alt_24"
app:key="export_preferences"
app:key="backup"
app:title="@string/backup"/>
<Preference
app:icon="@drawable/baseline_restore_page_24"
app:key="import_preferences"
app:key="restore"
app:title="@string/restore"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/about" >
<Preference
app:icon="@drawable/ic_discord"
app:key="discord"
@ -434,11 +80,6 @@
android:data="https://github.com/deniscerri/ytdlnis" />
</Preference>
<Preference
app:icon="@drawable/ic_info"
app:key="version"
app:title="@string/version"/>
</PreferenceCategory>

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:icon="@drawable/ic_update"
app:key="update_ytdl"
app:summary="@string/ytdl_update_hint"
app:title="@string/update_ytdl" />
<Preference
app:icon="@drawable/ic_info"
app:key="ytdl-version"
app:title="@string/ytdl_version"/>
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
android:defaultValue="false"
app:icon="@drawable/ic_nightly"
app:key="nightly_ytdl"
android:title="@string/update_ytdl_nightly"
/>
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_format"
android:key="update_formats"
app:summary="@string/update_formats_summary"
app:title="@string/update_formats" />
<ListPreference
android:defaultValue="yt-dlp"
android:entries="@array/formats_source"
android:entryValues="@array/formats_source_values"
android:icon="@drawable/baseline_manage_search_24"
app:key="formats_source"
app:useSimpleSummaryProvider="true"
app:title="@string/format_source" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:icon="@drawable/ic_update_app"
android:key="update_app"
app:summary="@string/update_app_summary"
app:title="@string/update_app" />
<Preference
app:icon="@drawable/ic_info"
app:key="version"
app:title="@string/version"/>
</PreferenceScreen>