- added subtitle language codes suggestions in the settings page
- made the extractor chips in history page Sentence case
- added a changelog screen where you can see recent releases and you can download the apks from it too
- prevented app from crashing when trying to backup from a corrupted backup
- added uploader_id as fallback for author data fetching in yt-dlp in case others are empty
- fixed null pointer exception when running the update multiple items formats worker
- added the seconds where the cut starts on downloads with cuts in them
- made autonumbers be normal numbers instead of being 5 digits
- fixed filename templates in cut files and added the index in the beginning and fixed bugs if the users left the template as empty
- added 240p as a generic format
This commit is contained in:
zaednasr 2023-12-17 21:02:04 +01:00
parent f0c88dfc73
commit fba26b50d0
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
26 changed files with 624 additions and 342 deletions

View file

@ -9,8 +9,8 @@ plugins {
def properties = new Properties() def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 6 def versionMinor = 7
def versionPatch = 9 def versionPatch = 0
def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def versionExt = "" def versionExt = ""

View file

@ -339,10 +339,6 @@ class MainActivity : BaseActivity() {
window.statusBarColor = getColor(android.R.color.transparent) window.statusBarColor = getColor(android.R.color.transparent)
incognitoHeader.visibility = View.GONE incognitoHeader.visibility = View.GONE
} }
//check logs option to show in tablet navbar
if (navigationView is NavigationView){
(navigationView as NavigationView).menu.findItem(R.id.downloadLogListFragment).isVisible = preferences.getBoolean("log_downloads", false)
}
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {

View file

@ -1,6 +1,7 @@
package com.deniscerri.ytdlnis.database.models package com.deniscerri.ytdlnis.database.models
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import java.util.Date
data class GithubRelease( data class GithubRelease(
@SerializedName(value = "html_url") @SerializedName(value = "html_url")
@ -9,6 +10,8 @@ data class GithubRelease(
var tag_name: String, var tag_name: String,
@SerializedName(value = "body") @SerializedName(value = "body")
var body: String, var body: String,
@SerializedName(value = "published_at")
var published_at: Date,
@SerializedName(value = "assets") @SerializedName(value = "assets")
var assets: List<GithubReleaseAsset> var assets: List<GithubReleaseAsset>
) )

View file

@ -64,7 +64,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
val infoUtil = InfoUtil(context) val infoUtil = InfoUtil(context)
val items : ArrayList<ResultItem?> = arrayListOf() val items : ArrayList<ResultItem?> = arrayListOf()
do { do {
val tmp = infoUtil.getPlaylist(query, nextPageToken) val tmp = infoUtil.getPlaylist(query, nextPageToken, if (items.isNotEmpty()) items[0]?.playlistTitle ?: "" else "")
items.addAll(tmp.videos) items.addAll(tmp.videos)
val tmpToken = tmp.nextPageToken val tmpToken = tmp.nextPageToken
if (tmpToken.isEmpty()) break if (tmpToken.isEmpty()) break

View file

@ -354,25 +354,35 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
} }
fun switchDownloadType(list: List<DownloadItem>, type: Type) : List<DownloadItem>{ fun switchDownloadType(list: List<DownloadItem>, type: Type) : List<DownloadItem>{
val updatedDownloadPath : String = when(type){
Type.audio -> sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())!!
Type.video -> sharedPreferences.getString("video_path", FileUtil.getDefaultVideoPath())!!
Type.command -> sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())!!
else -> ""
}
list.forEach { list.forEach {
val format = getFormat(it.allFormats, type) val format = getFormat(it.allFormats, type)
it.format = format it.format = format
val currentDownloadPath = when(it.type){
Type.audio -> sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())
Type.video -> sharedPreferences.getString("video_path", FileUtil.getDefaultVideoPath())
Type.command -> sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
else -> ""
}
if (it.downloadPath == currentDownloadPath) it.downloadPath = updatedDownloadPath
var updatedDownloadPath: String = ""
var container: String = ""
when(type){
Type.audio -> {
updatedDownloadPath = sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())!!
container = sharedPreferences.getString("audio_format", "")!!
}
Type.video -> {
updatedDownloadPath = sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())!!
container = sharedPreferences.getString("audio_format", "")!!
}
Type.command -> {
updatedDownloadPath = sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())!!
container = ""
}
else -> {
updatedDownloadPath = ""
}
}
it.downloadPath = updatedDownloadPath
it.type = type it.type = type
it.container = container
} }
return list return list
} }
@ -443,7 +453,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
requirements.add {it: Format -> audioFormatIDPreference.contains(it.format_id)} requirements.add {it: Format -> audioFormatIDPreference.contains(it.format_id)}
sharedPreferences.getString("audio_language", "")?.apply { sharedPreferences.getString("audio_language", "")?.apply {
if (this.isNotEmpty()){ if (this.isNotBlank()){
requirements.add { it: Format -> it.lang == this } requirements.add { it: Format -> it.lang == this }
} }
} }

View file

@ -275,10 +275,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
val items = mutableListOf<DownloadItem>() val items = mutableListOf<DownloadItem>()
ids?.forEach { ids?.forEach {
items.add(downloadViewModel.getItemByID(it)) items.add(downloadViewModel.getItemByID(it))
downloadViewModel.deleteDownload(it)
}
withContext(Dispatchers.Main){
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
Pair("downloads", items)
))
} }
findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
Pair("downloads", items)
))
} }
} }
@ -449,96 +452,100 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private fun updateSearchViewItems(searchQuery: Editable?, linkYouCopied: View?) = lifecycleScope.launch(Dispatchers.Main) { private fun updateSearchViewItems(searchQuery: Editable?, linkYouCopied: View?) = lifecycleScope.launch(Dispatchers.Main) {
searchSuggestionsLinearLayout!!.visibility = GONE lifecycleScope.launch {
searchHistoryLinearLayout!!.visibility = GONE searchSuggestionsLinearLayout!!.visibility = GONE
searchSuggestionsLinearLayout!!.post { searchHistoryLinearLayout!!.visibility = GONE
searchSuggestionsLinearLayout!!.removeAllViews() searchSuggestionsLinearLayout!!.post {
} searchSuggestionsLinearLayout!!.removeAllViews()
searchHistoryLinearLayout!!.post {
searchHistoryLinearLayout!!.removeAllViews()
}
linkYouCopied!!.visibility = GONE
if (searchView!!.editText.text.isEmpty()){
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
}else{
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_plus, 0)
}
val history = withContext(Dispatchers.IO){
resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery!!) }
}
val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){
infoUtil!!.getSearchSuggestions(searchQuery.toString())
}else{
emptyList()
}
history.forEach { s ->
val v = LayoutInflater.from(fragmentContext).inflate(R.layout.search_suggestion_item, null)
val textView = v.findViewById<TextView>(R.id.suggestion_text)
textView.text = s
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0)
Handler(Looper.getMainLooper()).post {
searchHistoryLinearLayout!!.addView(
v
)
} }
textView.setOnClickListener { searchHistoryLinearLayout!!.post {
searchView!!.setText(s) searchHistoryLinearLayout!!.removeAllViews()
initSearch(searchView!!)
} }
textView.setOnLongClickListener {
val deleteDialog = MaterialAlertDialogBuilder(requireContext()) linkYouCopied!!.visibility = GONE
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + s + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } if (searchView!!.editText.text.isEmpty()){
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
searchHistoryLinearLayout!!.removeView(v) }else{
resultViewModel.removeSearchQueryFromHistory(s) searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_plus, 0)
}
val history = withContext(Dispatchers.IO){
resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery!!) }
}
val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){
withContext(Dispatchers.IO){
infoUtil!!.getSearchSuggestions(searchQuery.toString())
} }
deleteDialog.show() }else{
true emptyList()
} }
val mb = v.findViewById<ImageButton>(R.id.set_search_query_button) history.forEach { s ->
mb.setOnClickListener { val v = LayoutInflater.from(fragmentContext).inflate(R.layout.search_suggestion_item, null)
searchView!!.editText.setText(s) val textView = v.findViewById<TextView>(R.id.suggestion_text)
searchView!!.editText.setSelection(searchView!!.editText.length()) textView.text = s
} textView.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_restore, 0, 0, 0)
} Handler(Looper.getMainLooper()).post {
searchHistoryLinearLayout!!.isVisible = history.isNotEmpty() searchHistoryLinearLayout!!.addView(
if (linkYouCopied.findViewById<TextView>(R.id.suggestion_text).text.isNotEmpty()){ v
linkYouCopied.visibility = VISIBLE )
} }
textView.setOnClickListener {
searchView!!.setText(s)
initSearch(searchView!!)
}
textView.setOnLongClickListener {
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + s + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
searchHistoryLinearLayout!!.removeView(v)
resultViewModel.removeSearchQueryFromHistory(s)
}
deleteDialog.show()
true
}
suggestions.forEach { s -> val mb = v.findViewById<ImageButton>(R.id.set_search_query_button)
val v = LayoutInflater.from(fragmentContext) mb.setOnClickListener {
.inflate(R.layout.search_suggestion_item, null) searchView!!.editText.setText(s)
val textView = v.findViewById<TextView>(R.id.suggestion_text) searchView!!.editText.setSelection(searchView!!.editText.length())
textView.text = s }
Handler(Looper.getMainLooper()).post {
searchSuggestionsLinearLayout!!.addView(
v
)
} }
textView.setOnClickListener { searchHistoryLinearLayout!!.isVisible = history.isNotEmpty()
searchView!!.setText(s) if (linkYouCopied.findViewById<TextView>(R.id.suggestion_text).text.isNotEmpty()){
initSearch(searchView!!) linkYouCopied.visibility = VISIBLE
} }
val mb = v.findViewById<ImageButton>(R.id.set_search_query_button)
mb.setOnClickListener {
searchView!!.editText.setText(s)
searchView!!.editText.setSelection(searchView!!.editText.length())
}
}
searchSuggestionsLinearLayout!!.isVisible = suggestions.isNotEmpty()
if (Patterns.WEB_URL.matcher(searchView!!.editText.text).matches()){ suggestions.forEach { s ->
providersChipGroup?.visibility = GONE val v = LayoutInflater.from(fragmentContext)
chipGroupDivider?.visibility = GONE .inflate(R.layout.search_suggestion_item, null)
}else{ val textView = v.findViewById<TextView>(R.id.suggestion_text)
providersChipGroup?.visibility = VISIBLE textView.text = s
chipGroupDivider?.visibility = VISIBLE Handler(Looper.getMainLooper()).post {
searchSuggestionsLinearLayout!!.addView(
v
)
}
textView.setOnClickListener {
searchView!!.setText(s)
initSearch(searchView!!)
}
val mb = v.findViewById<ImageButton>(R.id.set_search_query_button)
mb.setOnClickListener {
searchView!!.editText.setText(s)
searchView!!.editText.setSelection(searchView!!.editText.length())
}
}
searchSuggestionsLinearLayout!!.isVisible = suggestions.isNotEmpty()
if (Patterns.WEB_URL.matcher(searchView!!.editText.text).matches()){
providersChipGroup?.visibility = GONE
chipGroupDivider?.visibility = GONE
}else{
providersChipGroup?.visibility = VISIBLE
chipGroupDivider?.visibility = VISIBLE
}
} }
} }

View file

@ -128,7 +128,7 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit
} else if (item.type == DownloadViewModel.Type.video) { } else if (item.type == DownloadViewModel.Type.video) {
if (filePresent) btn.setImageResource(R.drawable.ic_video_downloaded) else btn.setImageResource(R.drawable.ic_video) if (filePresent) btn.setImageResource(R.drawable.ic_video_downloaded) else btn.setImageResource(R.drawable.ic_video)
}else{ }else{
btn.setImageResource(R.drawable.ic_terminal) if (filePresent) btn.setImageResource(R.drawable.ic_terminal) else btn.setImageResource(R.drawable.baseline_code_off_24)
} }
if (btn.hasOnClickListeners()) btn.setOnClickListener(null) if (btn.hasOnClickListeners()) btn.setOnClickListener(null)
btn.isClickable = filePresent btn.isClickable = filePresent

View file

@ -253,9 +253,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val ids = mutableListOf<Long>() val ids = mutableListOf<Long>()
runBlocking { runBlocking {
items.map { it.status = DownloadRepository.Status.Saved.toString() } items.map { it.status = DownloadRepository.Status.Saved.toString() }
items.forEach { ids.addAll(downloadViewModel.repository.insertAll(items))
ids.add(downloadViewModel.repository.insert(it))
}
} }
val id = System.currentTimeMillis().toInt() val id = System.currentTimeMillis().toInt()
val workRequest = OneTimeWorkRequestBuilder<UpdatePlaylistFormatsWorker>() val workRequest = OneTimeWorkRequestBuilder<UpdatePlaylistFormatsWorker>()

View file

@ -16,6 +16,7 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout import com.facebook.shimmer.ShimmerFrameLayout
@ -147,19 +148,30 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
updateFormatsJob = launch(Dispatchers.IO) { updateFormatsJob = launch(Dispatchers.IO) {
//simple download //simple download
if (items.size == 1) { if (items.size == 1) {
val res = infoUtil.getFormats(items.first()!!.url) kotlin.runCatching {
res.filter { it.format_note != "storyboard" } val res = infoUtil.getFormats(items.first()!!.url)
chosenFormats = if (items.first()?.type == Type.audio) { res.filter { it.format_note != "storyboard" }
res.filter { it.format_note.contains("audio", ignoreCase = true) } chosenFormats = if (items.first()?.type == Type.audio) {
} else { res.filter { it.format_note.contains("audio", ignoreCase = true) }
res } else {
} res
if (chosenFormats.isEmpty()) throw Exception() }
if (chosenFormats.isEmpty()) throw Exception()
formats = listOf(res) formats = listOf(res)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
listener.onFormatsUpdated(formats) listener.onFormatsUpdated(formats)
}
}.onFailure { err ->
withContext(Dispatchers.Main){
UiUtil.handleResultResponse(requireActivity(), ResultViewModel.ResultsUiState(
false,
Pair(R.string.no_results, err.message.toString()),
mutableListOf(Pair(R.string.copy_log, ResultViewModel.ResultAction.COPY_LOG))
), closed = {})
}
} }
//list format filtering //list format filtering
}else{ }else{
var progress = "0/${items.size}" var progress = "0/${items.size}"

View file

@ -26,6 +26,8 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.forEach import androidx.core.view.forEach
@ -410,7 +412,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
val w = websites[i] val w = websites[i]
if (w == "null" || w.isEmpty()) continue if (w == "null" || w.isEmpty()) continue
val tmp = layoutinflater!!.inflate(R.layout.filter_chip, websiteGroup, false) as Chip val tmp = layoutinflater!!.inflate(R.layout.filter_chip, websiteGroup, false) as Chip
tmp.text = w tmp.text = w.replaceFirstChar { if (it.isLowerCase()) it.titlecase(java.util.Locale.getDefault()) else it.toString() }
tmp.id = i tmp.id = i
tmp.setOnClickListener { tmp.setOnClickListener {
Log.e(TAG, tmp.isChecked.toString()) Log.e(TAG, tmp.isChecked.toString())

View file

@ -1,19 +1,15 @@
package com.deniscerri.ytdlnis.ui.more.settings package com.deniscerri.ytdlnis.ui.more.settings
import android.app.Activity
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
@ -84,6 +80,8 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
} }
ThemeUtil.updateTheme(requireActivity() as AppCompatActivity) ThemeUtil.updateTheme(requireActivity() as AppCompatActivity)
val intent = Intent(requireContext(), MainActivity::class.java) val intent = Intent(requireContext(), MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this.startActivity(intent) this.startActivity(intent)
requireActivity().finishAffinity() requireActivity().finishAffinity()
true true
@ -93,6 +91,8 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
Preference.OnPreferenceChangeListener { _: Preference?, _: Any -> Preference.OnPreferenceChangeListener { _: Preference?, _: Any ->
ThemeUtil.updateTheme(requireActivity() as AppCompatActivity) ThemeUtil.updateTheme(requireActivity() as AppCompatActivity)
val intent = Intent(requireContext(), MainActivity::class.java) val intent = Intent(requireContext(), MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this.startActivity(intent) this.startActivity(intent)
requireActivity().finishAffinity() requireActivity().finishAffinity()
true true
@ -101,6 +101,8 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
Preference.OnPreferenceChangeListener { _: Preference?, _: Any -> Preference.OnPreferenceChangeListener { _: Preference?, _: Any ->
ThemeUtil.updateTheme(requireActivity() as AppCompatActivity) ThemeUtil.updateTheme(requireActivity() as AppCompatActivity)
val intent = Intent(requireContext(), MainActivity::class.java) val intent = Intent(requireContext(), MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this.startActivity(intent) this.startActivity(intent)
requireActivity().finishAffinity() requireActivity().finishAffinity()

View file

@ -9,6 +9,8 @@ import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.util.LayoutDirection import android.util.LayoutDirection
import android.util.Log import android.util.Log
import android.view.View
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit import androidx.core.content.edit
@ -36,10 +38,12 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UpdateUtil import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
@ -66,7 +70,6 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
private lateinit var cookieViewModel: CookieViewModel private lateinit var cookieViewModel: CookieViewModel
private lateinit var commandTemplateViewModel: CommandTemplateViewModel private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private var version: Preference? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey) setPreferencesFromResource(R.xml.root_preferences, rootKey)
@ -149,21 +152,40 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
getString(R.string.ok) getString(R.string.ok)
) { _: DialogInterface?, _: Int -> ) { _: DialogInterface?, _: Int ->
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
if (checkedItems.all { !it }){
withContext(Dispatchers.Main){
Snackbar.make(requireView(), R.string.select_backup_categories, Snackbar.LENGTH_SHORT).show()
}
return@launch
}
val json = JsonObject() val json = JsonObject()
json.addProperty("app", "YTDLnis_backup") json.addProperty("app", "YTDLnis_backup")
for (i in 0 until checkedItems.size) { for (i in 0 until checkedItems.size) {
if (checkedItems[i]) { if (checkedItems[i]) {
when(values[i]){ runCatching {
"settings" -> json.add("settings", backupSettings(preferences)) when(values[i]){
"downloads" -> json.add("downloads", backupHistory()) "settings" -> json.add("settings", backupSettings(preferences))
"queued" -> json.add("queued", backupQueuedDownloads() ) "downloads" -> json.add("downloads", backupHistory())
"cancelled" -> json.add("cancelled", backupCancelledDownloads() ) "queued" -> json.add("queued", backupQueuedDownloads() )
"errored" -> json.add("errored", backupErroredDownloads() ) "cancelled" -> json.add("cancelled", backupCancelledDownloads() )
"saved" -> json.add("saved", backupSavedDownloads() ) "errored" -> json.add("errored", backupErroredDownloads() )
"cookies" -> json.add("cookies", backupCookies() ) "saved" -> json.add("saved", backupSavedDownloads() )
"templates" -> json.add("templates", backupCommandTemplates() ) "cookies" -> json.add("cookies", backupCookies() )
"shortcuts" -> json.add("shortcuts", backupShortcuts() ) "templates" -> json.add("templates", backupCommandTemplates() )
"searchHistory" -> json.add("search_history", backupSearchHistory() ) "shortcuts" -> json.add("shortcuts", backupShortcuts() )
"searchHistory" -> json.add("search_history", backupSearchHistory() )
}
}.onFailure {err ->
withContext(Dispatchers.Main){
val snack = Snackbar.make(requireView(), err.message ?: requireContext().getString(R.string.errored), Snackbar.LENGTH_LONG)
val snackbarView: View = snack.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
snack.setAction(android.R.string.copy){
UiUtil.copyToClipboard(err.message ?: requireContext().getString(R.string.errored), requireActivity())
}
snack.show()
}
} }
} }
} }
@ -176,8 +198,12 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
Calendar.DAY_OF_MONTH)} [${currentTime.get(Calendar.MILLISECOND)}.json") Calendar.DAY_OF_MONTH)} [${currentTime.get(Calendar.MILLISECOND)}.json")
saveFile.delete() saveFile.delete()
saveFile.createNewFile() saveFile.createNewFile()
saveFile.writeText(Gson().toJson(json)) saveFile.writeText(GsonBuilder().setPrettyPrinting().create().toJson(json))
Snackbar.make(requireView(), getString(R.string.backup_created_successfully), Snackbar.LENGTH_LONG).show() val s = Snackbar.make(requireView(), getString(R.string.backup_created_successfully), Snackbar.LENGTH_LONG)
s.setAction(R.string.Open_File){
UiUtil.openFileIntent(requireContext(), saveFile.absolutePath)
}
s.show()
} }
} }
@ -201,25 +227,6 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
true true
} }
version = findPreference("version")
val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir
version!!.summary = "${BuildConfig.VERSION_NAME} [${nativeLibraryDir?.split("/lib/")?.get(1)}]"
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 fun backupSettings(preferences: SharedPreferences) : JsonArray { private fun backupSettings(preferences: SharedPreferences) : JsonArray {

View file

@ -3,22 +3,38 @@ package com.deniscerri.ytdlnis.ui.more.settings
import android.os.Bundle import android.os.Bundle
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.UiUtil
class ProcessingSettingsFragment : BaseSettingsFragment() { class ProcessingSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.processing override val title: Int = R.string.processing
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.processing_preferences, rootKey) setPreferencesFromResource(R.xml.processing_preferences, rootKey)
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = prefs.edit()
val preferredFormatID : EditTextPreference? = findPreference("format_id") val preferredFormatID : EditTextPreference? = findPreference("format_id")
val preferredFormatIDAudio : EditTextPreference? = findPreference("format_id_audio") val preferredFormatIDAudio : EditTextPreference? = findPreference("format_id_audio")
val subtitleLanguages : Preference? = findPreference("subs_lang")
preferredFormatID?.title = "${getString(R.string.preferred_format_id)} [${getString(R.string.video)}]" preferredFormatID?.title = "${getString(R.string.preferred_format_id)} [${getString(R.string.video)}]"
preferredFormatID?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]" preferredFormatID?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
preferredFormatIDAudio?.title = "${getString(R.string.preferred_format_id)} [${getString(R.string.audio)}]" preferredFormatIDAudio?.title = "${getString(R.string.preferred_format_id)} [${getString(R.string.audio)}]"
preferredFormatIDAudio?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]" preferredFormatIDAudio?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]"
subtitleLanguages?.summary = prefs.getString("subs_lang", "en.*,.*-orig")!!
subtitleLanguages?.setOnPreferenceClickListener {
UiUtil.showSubtitleLanguagesDialog(requireActivity(), prefs.getString("subs_lang", "en.*,.*-orig")!!){
editor.putString("subs_lang", it)
editor.apply()
subtitleLanguages.summary = it
}
true
}
} }
} }

View file

@ -1,15 +1,20 @@
package com.deniscerri.ytdlnis.ui.more.settings package com.deniscerri.ytdlnis.ui.more.settings
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.util.UpdateUtil import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class UpdateSettingsFragment : BaseSettingsFragment() { class UpdateSettingsFragment : BaseSettingsFragment() {
@ -17,6 +22,8 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
private var updateYTDL: Preference? = null private var updateYTDL: Preference? = null
private var ytdlVersion: Preference? = null private var ytdlVersion: Preference? = null
private var updateUtil: UpdateUtil? = null private var updateUtil: UpdateUtil? = null
private var version: Preference? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -39,33 +46,44 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
Snackbar.make(requireView(), Snackbar.make(requireView(),
requireContext().getString(R.string.ytdl_updating_started), requireContext().getString(R.string.ytdl_updating_started),
Snackbar.LENGTH_LONG).show() Snackbar.LENGTH_LONG).show()
when (updateUtil!!.updateYoutubeDL()) { runCatching {
YoutubeDL.UpdateStatus.DONE -> { when (updateUtil!!.updateYoutubeDL()) {
Snackbar.make(requireView(), YoutubeDL.UpdateStatus.DONE -> {
requireContext().getString(R.string.ytld_update_success), Snackbar.make(requireView(),
Snackbar.LENGTH_LONG).show() requireContext().getString(R.string.ytld_update_success),
Snackbar.LENGTH_LONG).show()
YoutubeDL.getInstance().version(context)?.let { YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it) editor.putString("ytdl-version", it)
editor.apply() editor.apply()
ytdlVersion!!.summary = it 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()
} }
} }
YoutubeDL.UpdateStatus.ALREADY_UP_TO_DATE -> Snackbar.make(requireView(), }.onFailure {
requireContext().getString(R.string.you_are_in_latest_version), val msg = it.message ?: requireContext().getString(R.string.errored)
Snackbar.LENGTH_LONG).show() val snackBar = Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG)
else -> { snackBar.setAction(android.R.string.copy){
Snackbar.make(requireView(), UiUtil.copyToClipboard(msg, requireActivity())
requireContext().getString(R.string.errored),
Snackbar.LENGTH_LONG).show()
} }
val snackbarView: View = snackBar.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
snackBar.show()
} }
} }
true true
} }
val pipedInstance = findPreference<Preference>("piped_instance") findPreference<Preference>("piped_instance")?.setOnPreferenceClickListener {
pipedInstance?.setOnPreferenceClickListener {
UiUtil.showPipedInstancesDialog(requireActivity(), preferences.getString("piped_instance", "")!!){ UiUtil.showPipedInstancesDialog(requireActivity(), preferences.getString("piped_instance", "")!!){
editor.putString("piped_instance", it) editor.putString("piped_instance", it)
editor.apply() editor.apply()
@ -73,6 +91,32 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
true true
} }
findPreference<Preference>("changelog")?.setOnPreferenceClickListener {
lifecycleScope.launch(Dispatchers.IO) {
updateUtil?.showChangeLog(requireActivity())
}
true
}
version = findPreference("version")
val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir
version!!.summary = "${BuildConfig.VERSION_NAME} [${nativeLibraryDir?.split("/lib/")?.get(1)}]"
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

@ -13,6 +13,7 @@ import android.util.DisplayMetrics
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.neo.highlight.core.Highlight import com.neo.highlight.core.Highlight

View file

@ -100,7 +100,7 @@ class InfoUtil(private val context: Context) {
} }
@Throws(JSONException::class) @Throws(JSONException::class)
fun getPlaylist(id: String, nextPageToken: String): PlaylistTuple { fun getPlaylist(id: String, nextPageToken: String, playlistName: String): PlaylistTuple {
try{ try{
val items = arrayListOf<ResultItem?>() val items = arrayListOf<ResultItem?>()
// -------------- PIPED API FUNCTION ------------------- // -------------- PIPED API FUNCTION -------------------
@ -117,7 +117,7 @@ class InfoUtil(private val context: Context) {
kotlin.runCatching { kotlin.runCatching {
val obj = dataArray.getJSONObject(i) val obj = dataArray.getJSONObject(i)
val itm = createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url")) val itm = createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url"))
itm?.playlistTitle = res.getString("name") itm?.playlistTitle = runCatching { res.getString("name") }.getOrElse { playlistName }
itm?.playlistURL = "https://www.youtube.com/playlist?list=$id" itm?.playlistURL = "https://www.youtube.com/playlist?list=$id"
itm?.playlistIndex = (i+1) itm?.playlistIndex = (i+1)
items.add(itm) items.add(itm)
@ -425,65 +425,61 @@ class InfoUtil(private val context: Context) {
val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?") val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?")
val m = p.matcher(url) val m = p.matcher(url)
val formatSource = sharedPreferences.getString("formats_source", "yt-dlp") val formatSource = sharedPreferences.getString("formats_source", "yt-dlp")
return try { if (m.find() && formatSource == "piped"){
if(m.find() && formatSource == "piped"){ return try {
val id = getIDFromYoutubeURL(url) val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id") val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) getFromYTDL(url)[0]!! if (res.length() == 0) getFromYTDL(url)[0]!!
val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id") val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id")
item!!.formats item!!.formats
}else{ }catch(e: Exception) {
getFormatsFromYTDL(url) if (e is CancellationException) throw e
return getFormatsFromYTDL(url)
} }
}catch (e: Exception){ }else{
if (e is CancellationException) throw e return getFormatsFromYTDL(url)
getFormatsFromYTDL(url)
} }
} }
private fun getFormatsFromYTDL(url: String) : List<Format> { private fun getFormatsFromYTDL(url: String) : List<Format> {
try { val request = YoutubeDLRequest(url)
val request = YoutubeDLRequest(url) request.addOption("--print", "%(formats)s")
request.addOption("--print", "%(formats)s") request.addOption("--print", "%(duration)s")
request.addOption("--print", "%(duration)s") request.addOption("--skip-download")
request.addOption("--skip-download") request.addOption("-R", "1")
request.addOption("-R", "1") request.addOption("--compat-options", "manifest-filesize-approx")
request.addOption("--compat-options", "manifest-filesize-approx") request.addOption("--socket-timeout", "5")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("use_cookies", false)){ if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) request.addOption("--cookies", it)
}
val useHeader = sharedPreferences.getBoolean("use_header", false)
val header = sharedPreferences.getString("useragent_header", "")
if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
}
} }
val proxy = sharedPreferences.getString("proxy", "") val useHeader = sharedPreferences.getBoolean("use_header", false)
if (proxy!!.isNotBlank()) { val header = sharedPreferences.getString("useragent_header", "")
request.addOption("--proxy", proxy) if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
} }
val res = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = try {
val lineSeparator = System.getProperty("line.separator")
res.out.split(lineSeparator!!).toTypedArray()
} catch (e: Exception) {
arrayOf(res.out)
}
val json = results[0]
val jsonArray = JSONArray(json)
return parseYTDLFormats(jsonArray)
}catch (e: Exception){
return listOf()
} }
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()) {
request.addOption("--proxy", proxy)
}
val res = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = try {
val lineSeparator = System.getProperty("line.separator")
res.out.split(lineSeparator!!).toTypedArray()
} catch (e: Exception) {
arrayOf(res.out)
}
val json = results[0]
val jsonArray = JSONArray(json)
return parseYTDLFormats(jsonArray)
} }
fun getFormatsMultiple(urls: List<String>, progress: (progress: List<Format>) -> Unit){ fun getFormatsMultiple(urls: List<String>, progress: (progress: List<Format>) -> Unit){
@ -548,8 +544,9 @@ class InfoUtil(private val context: Context) {
urls.forEach { urls.forEach {
val id = getIDFromYoutubeURL(it) val id = getIDFromYoutubeURL(it)
val res = genericRequest("$pipedURL/streams/$id") val res = genericRequest("$pipedURL/streams/$id")
val vid = createVideoFromPipedJSON(res, it) createVideoFromPipedJSON(res, it)?.apply {
progress(vid!!.formats) progress(this.formats)
}
} }
} }
@ -629,6 +626,9 @@ class InfoUtil(private val context: Context) {
author = if (jsonObject.has("channel")) jsonObject.getString("channel") else "" author = if (jsonObject.has("channel")) jsonObject.getString("channel") else ""
if (author.isEmpty() || author == "null"){ if (author.isEmpty() || author == "null"){
author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else "" author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else ""
if (author.isEmpty() || author == "null"){
author = if (jsonObject.has("uploader_id")) jsonObject.getString("uploader_id") else ""
}
} }
} }
var duration = "" var duration = ""
@ -791,6 +791,9 @@ class InfoUtil(private val context: Context) {
author = if (jsonObject.has("channel")) jsonObject.getString("channel") else "" author = if (jsonObject.has("channel")) jsonObject.getString("channel") else ""
if (author.isEmpty() || author == "null"){ if (author.isEmpty() || author == "null"){
author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else "" author = if (jsonObject.has("playlist_uploader")) jsonObject.getString("playlist_uploader") else ""
if (author.isEmpty() || author == "null"){
author = if (jsonObject.has("uploader_id")) jsonObject.getString("uploader_id") else ""
}
} }
} }
@ -837,7 +840,6 @@ class InfoUtil(private val context: Context) {
fun getSearchSuggestions(query: String): ArrayList<String> { fun getSearchSuggestions(query: String): ArrayList<String> {
val url = "https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&client=firefox&q=$query" 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) val res = genericArrayRequest(url)
Log.e("aa", res.toString()) Log.e("aa", res.toString())
if (res.length() == 0) return ArrayList() if (res.length() == 0) return ArrayList()
@ -1086,8 +1088,10 @@ class InfoUtil(private val context: Context) {
if(downloadItem.title.isNotBlank()){ if(downloadItem.title.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "title", ".+", downloadItem.title)) request.addCommands(listOf("--replace-in-metadata", "title", ".+", downloadItem.title))
} }
request.addOption("--parse-metadata", "%(channel,uploader,creator,artist|null)s:%(uploader)s")
request.addOption("--parse-metadata", "%(uploader)s:%(album_artist)s")
if (downloadItem.author.isNotBlank()){ if (downloadItem.author.isNotBlank()){
request.addOption("--parse-metadata", "%(artist,uploader,creator,channel|null)s:%(uploader)s")
request.addCommands(listOf("--replace-in-metadata", "uploader", ".+", downloadItem.author)) request.addCommands(listOf("--replace-in-metadata", "uploader", ".+", downloadItem.author))
request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$","")) request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$",""))
} }
@ -1104,9 +1108,13 @@ class InfoUtil(private val context: Context) {
request.addOption("--force-keyframes-at-cuts") request.addOption("--force-keyframes-at-cuts")
} }
} }
filenameTemplate += " %(section_title|)s " if (filenameTemplate.isBlank()){
filenameTemplate = "%(section_title& {}|)s %(title)s"
}else{
filenameTemplate += " %(section_title& {}|)s "
}
if (downloadItem.downloadSections.split(";").size > 1){ if (downloadItem.downloadSections.split(";").size > 1){
filenameTemplate += "%(autonumber)s" filenameTemplate = "%(autonumber)d. %(section_start& {}|)s $filenameTemplate"
} }
request.addOption("--output-na-placeholder", " ") request.addOption("--output-na-placeholder", " ")
} }
@ -1145,7 +1153,7 @@ class InfoUtil(private val context: Context) {
val preferredAudioCodec = sharedPreferences.getString("audio_codec", "")!! val preferredAudioCodec = sharedPreferences.getString("audio_codec", "")!!
val aCodecPrefIndex = context.getStringArray(R.array.audio_codec_values).indexOf(preferredAudioCodec) val aCodecPrefIndex = context.getStringArray(R.array.audio_codec_values).indexOf(preferredAudioCodec)
val aCodecPref = context.getStringArray(R.array.audio_codec_values_ytdlp)[aCodecPrefIndex] val aCodecPref = runCatching { context.getStringArray(R.array.audio_codec_values_ytdlp)[aCodecPrefIndex] }.getOrElse { "" }
when(type){ when(type){
DownloadViewModel.Type.audio -> { DownloadViewModel.Type.audio -> {
@ -1162,14 +1170,14 @@ class InfoUtil(private val context: Context) {
val ext = downloadItem.container val ext = downloadItem.container
if (audioQualityId.isNotBlank()) { if (audioQualityId.isNotBlank()) {
if (audioQualityId.contains("-") && !downloadItem.format.lang.isNullOrBlank() && downloadItem.format.lang != "None"){ if (audioQualityId.contains("-") && !downloadItem.format.lang.isNullOrBlank() && downloadItem.format.lang != "None"){
audioQualityId = "ba[format_id~='^(${audioQualityId.split("-")[0]})'][language=${downloadItem.format.lang}]/ba/b" audioQualityId = "ba[format_id~='^(${audioQualityId.split("-")[0]})'][language^=${downloadItem.format.lang}]/ba/b"
} }
request.addOption("-f", audioQualityId) request.addOption("-f", audioQualityId)
}else{ }else{
//enters here if generic or quick downloaded with ba format //enters here if generic or quick downloaded with ba format
val preferredLanguage = sharedPreferences.getString("audio_language","")!! val preferredLanguage = sharedPreferences.getString("audio_language","")!!
if (preferredLanguage.isNotBlank()){ if (preferredLanguage.isNotBlank()){
request.addOption("-f", "ba[language=$preferredLanguage]/ba/b") request.addOption("-f", "ba[language^=$preferredLanguage]/ba/b")
} }
} }
request.addOption("-x") request.addOption("-x")
@ -1272,15 +1280,17 @@ class InfoUtil(private val context: Context) {
val defaultFormats = context.resources.getStringArray(R.array.video_formats_values) val defaultFormats = context.resources.getStringArray(R.array.video_formats_values)
val usingGenericFormat = defaultFormats.contains(videoF) || downloadItem.allFormats.isEmpty() || downloadItem.allFormats == getGenericVideoFormats(context.resources) val usingGenericFormat = defaultFormats.contains(videoF) || downloadItem.allFormats.isEmpty() || downloadItem.allFormats == getGenericVideoFormats(context.resources)
if (!usingGenericFormat){ if (!usingGenericFormat){
if (audioF.isNotBlank()) audioF = "+$audioF" // with audio removed
f.append("$videoF$audioF/") if (audioF.isBlank()){
if (preferredAudioCodec.isNotBlank()){ f.append("$videoF/bv/b")
f.append("$videoF+$aCodecPref/") }else{
} //with audio
f.append("$videoF/bv/b") audioF = "+$audioF"
f.append("$videoF$audioF/$videoF+ba/$videoF/b")
if (audioF.count("+") > 1){ if (audioF.count("+") > 1){
request.addOption("--audio-multistreams") request.addOption("--audio-multistreams")
}
} }
}else{ }else{
if (videoF == context.resources.getString(R.string.best_quality) || videoF == "best") { if (videoF == context.resources.getString(R.string.best_quality) || videoF == "best") {
@ -1306,8 +1316,7 @@ class InfoUtil(private val context: Context) {
.filter { it.isNotBlank() } .filter { it.isNotBlank() }
.ifEmpty { .ifEmpty {
val list = mutableListOf<String>() val list = mutableListOf<String>()
if (preferredAudioCodec.isNotBlank()) list.add("ba[acodec~='^($preferredAudioCodec)']") if (preferredAudioLanguage.isNotEmpty()) list.add("ba[language^=$preferredAudioLanguage]")
if (preferredAudioLanguage.isNotEmpty()) list.add("ba[language=$preferredAudioLanguage]")
list.add(audioF) list.add(audioF)
list list
}.apply { }.apply {
@ -1333,7 +1342,7 @@ class InfoUtil(private val context: Context) {
if (!downloadItem.videoPreferences.removeAudio){ if (!downloadItem.videoPreferences.removeAudio){
//build format with audio with preferred language //build format with audio with preferred language
if (preferredAudioLanguage.isNotBlank()){ if (preferredAudioLanguage.isNotBlank()){
val al = "$v+ba[language=$preferredAudioLanguage]/" val al = "$v+ba[language^=$preferredAudioLanguage]/"
if (!f.contains(al)) f.append(al) if (!f.contains(al)) f.append(al)
} }
//build format with best audio //build format with best audio
@ -1348,12 +1357,14 @@ class InfoUtil(private val context: Context) {
f.append("b") f.append("b")
} }
val formatSorting = StringBuilder("+hasaud") StringBuilder().apply {
if (vCodecPref.isNotBlank()) formatSorting.append(",vcodec:$vCodecPref") if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref")
if (aCodecPref.isNotBlank()) formatSorting.append(",acodec:$aCodecPref") if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref")
if (cont.isNotBlank()) formatSorting.append(",ext:$cont") if (cont.isNotBlank()) append(",ext:$cont")
if (this.isNotBlank()){
request.addOption("-S", formatSorting.toString()) request.addOption("-S", "+hasaud$this")
}
}
} }

View file

@ -896,7 +896,7 @@ object UiUtil {
) )
} }
private fun showSubtitleLanguagesDialog(context: Activity, currentValue: String, ok: (newValue: String) -> Unit){ fun showSubtitleLanguagesDialog(context: Activity, currentValue: String, ok: (newValue: String) -> Unit){
val builder = MaterialAlertDialogBuilder(context) val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(context.getString(R.string.subtitle_languages)) builder.setTitle(context.getString(R.string.subtitle_languages))
val view = context.layoutInflater.inflate(R.layout.subtitle_dialog, null) val view = context.layoutInflater.inflate(R.layout.subtitle_dialog, null)
@ -966,7 +966,7 @@ object UiUtil {
} }
private fun copyToClipboard(text: String, activity: Activity){ fun copyToClipboard(text: String, activity: Activity){
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(text, text) val clip = ClipData.newPlainText(text, text)
clipboard.setPrimaryClip(clip) clipboard.setPrimaryClip(clip)
@ -1454,6 +1454,21 @@ object UiUtil {
errDialog.show() errDialog.show()
} }
fun showErrorDialog(context: Context, it: String){
val title = context.getString(R.string.errored)
val errDialog = MaterialAlertDialogBuilder(context)
.setTitle(title)
.setMessage(it)
.setPositiveButton(android.R.string.copy) { d:DialogInterface?, _:Int ->
val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setText(it)
d?.dismiss()
}
errDialog.show()
}
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun handleDownloadsResponse(context: Activity, lifecycleScope: CoroutineScope, supportFragmentManager: FragmentManager, it: DownloadViewModel.DownloadsUiState, downloadViewModel: DownloadViewModel, historyViewModel: HistoryViewModel){ fun handleDownloadsResponse(context: Activity, lifecycleScope: CoroutineScope, supportFragmentManager: FragmentManager, it: DownloadViewModel.DownloadsUiState, downloadViewModel: DownloadViewModel, historyViewModel: HistoryViewModel){
val downloadAnywayAction = it.actions?.first { it.second == DownloadViewModel.DownloadsAction.DOWNLOAD_ANYWAY} val downloadAnywayAction = it.actions?.first { it.second == DownloadViewModel.DownloadsAction.DOWNLOAD_ANYWAY}
@ -1732,7 +1747,7 @@ object UiUtil {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val chipGroup = view.findViewById<ChipGroup>(R.id.filename_suggested_chipgroup) val chipGroup = view.findViewById<ChipGroup>(R.id.filename_suggested_chipgroup)
val chips = mutableListOf<Chip>() val chips = mutableListOf<Chip>()
val instances = InfoUtil(context).getPipedInstances() val instances = InfoUtil(context).getPipedInstances().ifEmpty { return@launch }
instances.forEach { s -> instances.forEach { s ->
val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
tmp.text = s tmp.text = s

View file

@ -1,6 +1,7 @@
package com.deniscerri.ytdlnis.util package com.deniscerri.ytdlnis.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.DownloadManager import android.app.DownloadManager
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
@ -13,8 +14,11 @@ import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.format.DateFormat
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.util.Log import android.util.Log
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat.startActivity import androidx.core.content.ContextCompat.startActivity
@ -22,6 +26,10 @@ import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.BuildConfig import com.deniscerri.ytdlnis.BuildConfig
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.GithubRelease import com.deniscerri.ytdlnis.database.models.GithubRelease
import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
@ -36,6 +44,8 @@ import java.io.File
import java.io.InputStreamReader import java.io.InputStreamReader
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.text.SimpleDateFormat
import java.util.Locale
class UpdateUtil(var context: Context) { class UpdateUtil(var context: Context) {
@ -152,6 +162,75 @@ class UpdateUtil(var context: Context) {
} }
} }
fun showChangeLog(activity: Activity){
runCatching {
val scrollView = ScrollView(activity)
val linearLayout = LinearLayout(activity)
linearLayout.orientation = LinearLayout.VERTICAL
val layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT
)
layoutParams.setMargins(20, 20, 20, 0)
scrollView.layoutParams = layoutParams
scrollView.addView(linearLayout)
getGithubReleases().forEach {
(activity.layoutInflater.inflate(R.layout.changelog_item, null) as MaterialCardView).apply {
this.layoutParams = layoutParams
findViewById<TextView>(R.id.version).text = it.tag_name
findViewById<TextView>(R.id.date).text = SimpleDateFormat(
DateFormat.getBestDateTimePattern(
Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(it.published_at.time)
val mdText = findViewById<TextView>(R.id.content)
val mw = Markwon.builder(context).usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.linkResolver { view, link ->
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
startActivity(context, browserIntent, Bundle())
}
}
}).build()
mw.setMarkdown(mdText, it.body)
val assetGroup = findViewById<ChipGroup>(R.id.assets)
it.assets.forEachIndexed { idx, c ->
val tmp = activity.layoutInflater.inflate(R.layout.suggestion_chip, assetGroup, false) as Chip
tmp.text = c.name
tmp.id = idx
tmp.setOnClickListener {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(c.browser_download_url))
startActivity(context, browserIntent, Bundle())
}
assetGroup!!.addView(tmp)
}
linearLayout.addView(this)
}
}
val changeLogDialog = MaterialAlertDialogBuilder(context)
.setTitle(activity.getString(R.string.changelog))
.setView(scrollView)
.setIcon(R.drawable.ic_chapters)
.setNegativeButton(context.resources.getString(R.string.cancel)) { _: DialogInterface?, _: Int -> }
Handler(Looper.getMainLooper()).post {
changeLogDialog.show()
}
}.onFailure {
if (it.message != null){
Handler(Looper.getMainLooper()).post {
UiUtil.showErrorDialog(context, it.message!!)
}
}
}
}
private fun getGithubReleases(): List<GithubRelease> { private fun getGithubReleases(): List<GithubRelease> {
val url = "https://api.github.com/repos/deniscerri/ytdlnis/releases" val url = "https://api.github.com/repos/deniscerri/ytdlnis/releases"
val conn: HttpURLConnection val conn: HttpURLConnection
@ -188,15 +267,8 @@ class UpdateUtil(var context: Context) {
"master" to YoutubeDL.UpdateChannel._MASTER, "master" to YoutubeDL.UpdateChannel._MASTER,
) )
val channel = sharedPreferences.getString("ytdlp_source", "nightly") val channel = sharedPreferences.getString("ytdlp_source", "nightly")
YoutubeDL.getInstance().updateYoutubeDL(context, channelMap[channel] ?: YoutubeDL.UpdateChannel._NIGHTLY).apply {
try {
YoutubeDL.getInstance().updateYoutubeDL(context, channelMap[channel] ?: YoutubeDL.UpdateChannel._NIGHTLY).apply {
updatingYTDL = false
}
}catch (e: Exception){
e.printStackTrace()
updatingYTDL = false updatingYTDL = false
null
} }
} }

View file

@ -55,7 +55,7 @@ class UpdatePlaylistFormatsWorker(
runBlocking { runBlocking {
resDao.update(r) resDao.update(r)
dao.delete(d.id) dao.update(d)
} }
} }

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M19.17,12l-4.58,-4.59L16,6l6,6l-3.59,3.59L17,14.17L19.17,12zM1.39,4.22l4.19,4.19L2,12l6,6l1.41,-1.41L4.83,12L7,9.83l12.78,12.78l1.41,-1.41L2.81,2.81L1.39,4.22z"/>
</vector>

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_margin="10dp"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/version"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:fontFamily="monospace"
android:text="1.6.9"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:text="1.1.1970"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/version" />
<TextView
android:id="@+id/content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginBottom="10dp"
android:text="## WHATS NEW"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/date" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/assets"
android:layout_marginBottom="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:chipSpacingVertical="-7dp"
app:chipSpacingHorizontal="5dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/content"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1112,6 +1112,7 @@
<!--below is for preferred audio language, not related to app's language--> <!--below is for preferred audio language, not related to app's language-->
<string-array name="language_codes"> <string-array name="language_codes">
<item></item>
<item>af</item> <!-- Afrikaans --> <item>af</item> <!-- Afrikaans -->
<item>sq</item> <!-- Albanian --> <item>sq</item> <!-- Albanian -->
<item>am</item> <!-- Amharic --> <item>am</item> <!-- Amharic -->
@ -1222,6 +1223,7 @@
</string-array> </string-array>
<string-array name="language_names"> <string-array name="language_names">
<item>@string/defaultValue</item>
<item>Afrikaans</item> <item>Afrikaans</item>
<item>Albanian</item> <item>Albanian</item>
<item>Amharic</item> <item>Amharic</item>

View file

@ -327,4 +327,7 @@
<string name="buffer_size">Buffer Size</string> <string name="buffer_size">Buffer Size</string>
<string name="buffer_size_summary">Size of download buffer, e.g. 1024 or 16K (default is 1024)</string> <string name="buffer_size_summary">Size of download buffer, e.g. 1024 or 16K (default is 1024)</string>
<string name="preferred_audio_language">Preferred Audio Language</string> <string name="preferred_audio_language">Preferred Audio Language</string>
<string name="changelog">Changelog</string>
<string name="source">Source</string>
<string name="app">App</string>
</resources> </resources>

View file

@ -61,7 +61,7 @@
app:summary="@string/save_subs_desc" app:summary="@string/save_subs_desc"
app:title="@string/save_subs" /> app:title="@string/save_subs" />
<EditTextPreference <Preference
android:icon="@drawable/baseline_subject_24" android:icon="@drawable/baseline_subject_24"
app:key="subs_lang" app:key="subs_lang"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"

View file

@ -100,10 +100,6 @@
android:data="https://github.com/deniscerri/ytdlnis" /> android:data="https://github.com/deniscerri/ytdlnis" />
</Preference> </Preference>
<Preference
app:icon="@drawable/ic_info"
app:key="version"
app:title="@string/version"/>
</PreferenceCategory> </PreferenceCategory>

View file

@ -2,86 +2,104 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference <PreferenceCategory android:title="@string/source">
app:icon="@drawable/ic_update"
app:key="update_ytdl" <Preference
app:summary="@string/ytdl_update_hint" app:icon="@drawable/ic_info"
app:title="@string/update_ytdl" /> app:key="ytdl-version"
app:title="@string/ytdl_version"/>
<ListPreference
android:defaultValue="nightly"
android:entries="@array/ytdlp_source"
android:entryValues="@array/ytdlp_source_values"
android:icon="@drawable/baseline_source_24"
app:key="ytdlp_source"
app:useSimpleSummaryProvider="true"
app:title="@string/ytdl_source" />
<Preference
app:icon="@drawable/ic_update"
app:key="update_ytdl"
app:summary="@string/ytdl_update_hint"
app:title="@string/update_ytdl" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/ic_update"
android:key="auto_update_ytdlp"
app:title="@string/auto_update_ytdlp" />
<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" />
<ListPreference
android:defaultValue="filesize"
android:entries="@array/format_ordering"
android:entryValues="@array/format_ordering_values"
android:icon="@drawable/baseline_sort_24"
app:key="format_order"
app:useSimpleSummaryProvider="true"
app:title="@string/format_order" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:icon="@drawable/ic_format"
android:key="update_formats"
app:summary="@string/update_formats_summary"
app:title="@string/update_formats" />
<Preference
app:key="piped_instance"
app:defaultValue=""
android:summary="@string/piped_instance_summary"
app:title="@string/piped_instance" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/app">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_update_app"
android:key="update_app"
app:summary="@string/update_app_summary"
app:title="@string/update_app" />
<EditTextPreference
app:isPreferenceVisible="false"
app:key="skip_updates"
app:defaultValue="" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_update_app"
android:key="update_beta"
app:summary="@string/update_app_beta_summary"
app:title="@string/update_app_beta" />
<Preference
app:icon="@drawable/ic_chapters"
app:key="changelog"
app:title="@string/changelog" />
<SwitchPreferenceCompat <Preference
android:widgetLayout="@layout/preferece_material_switch" app:icon="@drawable/ic_info"
app:defaultValue="true" app:key="version"
app:icon="@drawable/ic_update" app:title="@string/version"/>
android:key="auto_update_ytdlp"
app:title="@string/auto_update_ytdlp" />
<Preference
app:icon="@drawable/ic_info"
app:key="ytdl-version"
app:title="@string/ytdl_version"/>
<ListPreference
android:defaultValue="nightly"
android:entries="@array/ytdlp_source"
android:entryValues="@array/ytdlp_source_values"
android:icon="@drawable/baseline_source_24"
app:key="ytdlp_source"
app:useSimpleSummaryProvider="true"
app:title="@string/ytdl_source" />
<SwitchPreferenceCompat </PreferenceCategory>
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
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" />
<ListPreference
android:defaultValue="filesize"
android:entries="@array/format_ordering"
android:entryValues="@array/format_ordering_values"
android:icon="@drawable/baseline_sort_24"
app:key="format_order"
app:useSimpleSummaryProvider="true"
app:title="@string/format_order" />
<Preference
app:key="piped_instance"
app:defaultValue=""
android:summary="@string/piped_instance_summary"
app:title="@string/piped_instance" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_update_app"
android:key="update_app"
app:summary="@string/update_app_summary"
app:title="@string/update_app" />
<EditTextPreference
app:isPreferenceVisible="false"
app:key="skip_updates"
app:defaultValue="" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_update_app"
android:key="update_beta"
app:summary="@string/update_app_beta_summary"
app:title="@string/update_app_beta" />
</PreferenceScreen> </PreferenceScreen>