searchable settings

pull/1129/head
deniscerri 7 months ago
parent 7498cdbad5
commit 00469962bb
No known key found for this signature in database
GPG Key ID: 95C43D517D830350

@ -0,0 +1,13 @@
package com.deniscerri.ytdl.database.models
import androidx.preference.Preference
import com.deniscerri.ytdl.ui.more.settings.SettingModule
data class SearchSettingsItem(
val preference: Preference,
val xmlId: Int,
val module: SettingModule?,
val groupTitle: String? = null,
val isHeader: Boolean = false,
var canRebind: Boolean = true
)

@ -8,9 +8,10 @@ import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.databinding.AppIconItemBinding
import com.deniscerri.ytdl.ui.more.settings.SettingHost
import com.deniscerri.ytdl.util.ThemeUtil
class IconsSheetAdapter(val activity: Activity) : RecyclerView.Adapter<IconsSheetAdapter.IconsSheetViewHolder>() {
class IconsSheetAdapter(val host: SettingHost) : RecyclerView.Adapter<IconsSheetAdapter.IconsSheetViewHolder>() {
class IconsSheetViewHolder(
val binding: AppIconItemBinding
@ -29,11 +30,12 @@ class IconsSheetAdapter(val activity: Activity) : RecyclerView.Adapter<IconsShee
iconIV.setImageResource(appIcon.iconResource)
iconName.text = root.context.getString(appIcon.nameResource)
root.setOnClickListener {
val preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(activity)
val preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(host.getHostContext())
preferences.edit().putString("ytdlnis_icon", appIcon.activityAlias).apply()
val theme = preferences.getString("ytdlnis_theme", "System")!!
ThemeUtil.updateAppIcon(activity, theme,appIcon.activityAlias)
activity.recreate()
ThemeUtil.updateAppIcon(host.getHostContext(), theme,appIcon.activityAlias)
host.refreshUI()
host.getHostContext().recreate()
}
}
}

@ -1,26 +1,41 @@
package com.deniscerri.ytdl.ui.more.settings
import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import android.os.Bundle
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceGroupAdapter
import androidx.preference.PreferenceManager
import androidx.preference.PreferenceScreen
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.databinding.TextinputBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
abstract class BaseSettingsFragment : PreferenceFragmentCompat() {
abstract class BaseSettingsFragment : PreferenceFragmentCompat(), SettingHost {
abstract val title: Int
override fun findPref(key: String) = findPreference<Preference>(key)
@SuppressLint("NotifyDataSetChanged")
override fun refreshUI() {
listView.adapter?.notifyDataSetChanged()
}
override fun getHostContext() = requireActivity()
override val activityResultDelegate = PreferenceActivityResultDelegate(this)
override fun requestGetParentFragmentManager() = parentFragmentManager
override fun requestRecreateActivity() = requireActivity().recreate()
override fun requestNavigate(id: Int) = findNavController().navigate(id)
override val hostViewModelStoreOwner by lazy {
this
}
override val hostLifecycleOwner by lazy {
this
}
override val hostView by lazy {
requireView()
}
override fun onStart() {
super.onStart()
(activity as? SettingsActivity)?.changeTopAppbarTitle(getString(title))
@ -49,74 +64,35 @@ abstract class BaseSettingsFragment : PreferenceFragmentCompat() {
//Thanks libretube
override fun onDisplayPreferenceDialog(preference: Preference) {
when (preference) {
/**
* Show a [MaterialAlertDialogBuilder] when the preference is a [ListPreference]
*/
is ListPreference -> {
// get the index of the previous selected item
val prefIndex = preference.entryValues.indexOf(preference.value)
MaterialAlertDialogBuilder(requireContext())
.setTitle(preference.title)
.setSingleChoiceItems(preference.entries, prefIndex) { dialog, index ->
// get the new ListPreference value
val newValue = preference.entryValues[index].toString()
// invoke the on change listeners
if (preference.callChangeListener(newValue)) {
preference.value = newValue
}
dialog.dismiss()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
is MultiSelectListPreference -> {
val selectedItems = preference.entryValues.map {
preference.values.contains(it)
}.toBooleanArray()
MaterialAlertDialogBuilder(requireContext())
.setTitle(preference.title)
.setMultiChoiceItems(preference.entries, selectedItems) { _, which, isChecked ->
selectedItems[which] = isChecked
}
.setPositiveButton(R.string.ok) { _, _ ->
val newValues = preference.entryValues
.filterIndexed { index, _ -> selectedItems[index] }
.map { it.toString() }
.toMutableSet()
if (preference.callChangeListener(newValues)) {
preference.values = newValues
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
is EditTextPreference -> {
val binding = TextinputBinding.inflate(layoutInflater)
binding.urlEdittext.setText(preference.text)
binding.urlTextinput.findViewById<TextInputLayout>(R.id.url_textinput).hint = preference.title
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(preference.title)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ ->
val newValue = binding.urlEdittext.text.toString()
if (preference.callChangeListener(newValue)) {
preference.text = newValue
}
}
.setNegativeButton(R.string.cancel, null)
dialog.show()
val imm = context?.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
binding.urlEdittext.setSelection(binding.urlEdittext.text!!.length)
binding.urlEdittext.postDelayed({
binding.urlEdittext.requestFocus()
imm.showSoftInput(binding.urlEdittext, 0)
val shownCustomDialog = DefaultPreferenceActions.onPreferenceDisplayDialog(requireActivity(), preference) {}
if (!shownCustomDialog) {
super.onDisplayPreferenceDialog(preference)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val keyToHighlight = arguments?.getString("highlight_key")
if (keyToHighlight != null) {
val adapter = listView.adapter as? PreferenceGroupAdapter
val position = adapter?.getPreferenceAdapterPosition(keyToHighlight) ?: -1
if (position != -1) {
listView.postDelayed({
listView.smoothScrollToPosition(position + 1)
listView.postDelayed({
val holder = listView.findViewHolderForAdapterPosition(position)
holder?.itemView?.let { itemView ->
val originalColor = itemView.background
itemView.setBackgroundColor(requireContext().getColor(android.R.color.system_control_highlight_light))
itemView.postDelayed({
itemView.background = originalColor
}, 1000)
}
}, 300)
}, 200)
}
/**
* Otherwise show the normal dialog, dialogs for other preference types are not supported yet
*/
else -> super.onDisplayPreferenceDialog(preference)
}
}
}

@ -0,0 +1,96 @@
package com.deniscerri.ytdl.ui.more.settings
import android.content.Context
import android.view.LayoutInflater
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.databinding.TextinputBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
object DefaultPreferenceActions {
fun onPreferenceDisplayDialog(context: Context, preference: Preference, callback: () -> Unit) : Boolean {
val layoutInflater = LayoutInflater.from(context)
return when (preference) {
/**
* Show a [MaterialAlertDialogBuilder] when the preference is a [ListPreference]
*/
is ListPreference -> {
// get the index of the previous selected item
val prefIndex = preference.entryValues.indexOf(preference.value)
MaterialAlertDialogBuilder(context)
.setTitle(preference.title)
.setSingleChoiceItems(preference.entries, prefIndex) { dialog, index ->
// get the new ListPreference value
val newValue = preference.entryValues[index].toString()
// invoke the on change listeners
if (preference.callChangeListener(newValue)) {
preference.value = newValue
}
callback()
dialog.dismiss()
}
.setNegativeButton(R.string.cancel, null)
.show()
true
}
is MultiSelectListPreference -> {
val selectedItems = preference.entryValues.map {
preference.values.contains(it)
}.toBooleanArray()
MaterialAlertDialogBuilder(context)
.setTitle(preference.title)
.setMultiChoiceItems(preference.entries, selectedItems) { _, which, isChecked ->
selectedItems[which] = isChecked
}
.setPositiveButton(R.string.ok) { _, _ ->
val newValues = preference.entryValues
.filterIndexed { index, _ -> selectedItems[index] }
.map { it.toString() }
.toMutableSet()
if (preference.callChangeListener(newValues)) {
preference.values = newValues
}
callback()
}
.setNegativeButton(R.string.cancel, null)
.show()
true
}
is EditTextPreference -> {
val binding = TextinputBinding.inflate(layoutInflater)
binding.urlEdittext.setText(preference.text)
binding.urlTextinput.findViewById<TextInputLayout>(R.id.url_textinput).hint = preference.title
val dialog = MaterialAlertDialogBuilder(context)
.setTitle(preference.title)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ ->
val newValue = binding.urlEdittext.text.toString()
if (preference.callChangeListener(newValue)) {
preference.text = newValue
}
callback()
}
.setNegativeButton(R.string.cancel, null)
dialog.show()
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
binding.urlEdittext.setSelection(binding.urlEdittext.text!!.length)
binding.urlEdittext.postDelayed({
binding.urlEdittext.requestFocus()
imm.showSoftInput(binding.urlEdittext, 0)
}, 300)
true
}
else -> false
}
}
}

@ -1,299 +0,0 @@
package com.deniscerri.ytdl.ui.more.settings
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.navigation.fragment.findNavController
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.CleanUpLeftoverDownloads
import com.deniscerri.ytdl.work.DownloadWorker
import java.util.Calendar
import java.util.concurrent.TimeUnit
class DownloadSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.downloads
private lateinit var archivePath: Preference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.downloading_preferences, rootKey)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val rememberDownloadType = findPreference<SwitchPreferenceCompat>("remember_download_type")
val downloadType = findPreference<ListPreference>("preferred_download_type")
downloadType?.isEnabled = rememberDownloadType?.isChecked == false
rememberDownloadType?.setOnPreferenceClickListener {
downloadType?.isEnabled = !rememberDownloadType.isChecked
true
}
val preventDuplicateDownloads = findPreference<ListPreference>("prevent_duplicate_downloads")
preventDuplicateDownloads?.setOnPreferenceChangeListener { _, newValue ->
archivePath.isVisible = newValue == "download_archive"
true
}
archivePath = findPreference("download_archive_path")!!
archivePath.summary = FileUtil.getDownloadArchivePath(requireContext())
archivePath.isVisible = preferences.getString("prevent_duplicate_downloads", "") == "download_archive"
archivePath.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)
archivePathResultLauncher.launch(intent)
true
}
val workManager = WorkManager.getInstance(requireContext())
val cleanupLeftoverDownloads = findPreference<Preference>("cleanup_leftover_downloads")
cleanupLeftoverDownloads?.setOnPreferenceChangeListener { preference, newValue ->
var nextTime : Calendar? = Calendar.getInstance()
when(newValue) {
"daily" -> nextTime?.add(Calendar.DAY_OF_WEEK, 1)
"weekly" -> nextTime?.add(Calendar.DAY_OF_WEEK, 7)
"monthly" -> nextTime?.add(Calendar.MONTH, 1)
else -> nextTime = null
}
if (nextTime == null) workManager.cancelAllWorkByTag("cleanup_leftover_downloads")
else {
val workConstraints = Constraints.Builder()
val allowMeteredNetworks = preferences.getBoolean("metered_networks", true)
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
val delay = nextTime.timeInMillis.minus(System.currentTimeMillis())
val workRequest = OneTimeWorkRequestBuilder<CleanUpLeftoverDownloads>()
.addTag("cleanup_leftover_downloads")
.setConstraints(workConstraints.build())
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
workManager.enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
true
}
val scheduler = AlarmScheduler(requireContext())
val useAlarmManagerInsteadOfWorkManager = findPreference<SwitchPreferenceCompat>("use_alarm_for_scheduling")
useAlarmManagerInsteadOfWorkManager?.setOnPreferenceChangeListener { preference, newValue ->
var allowChange = true
if (newValue as Boolean){
if (!scheduler.canSchedule() && Build.VERSION.SDK_INT >= 31){
Intent().also { intent ->
intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
requireContext().startActivity(intent)
}
allowChange = false
}
}
allowChange
}
val useScheduler = findPreference<SwitchPreferenceCompat>("use_scheduler")
val scheduleStart = findPreference<Preference>("schedule_start")
scheduleStart?.summary = preferences.getString("schedule_start", "00:00")
val scheduleEnd = findPreference<Preference>("schedule_end")
scheduleEnd?.summary = preferences.getString("schedule_end", "05:00")
useScheduler?.setOnPreferenceChangeListener { preference, newValue ->
var allowChange = true
if (newValue as Boolean){
if (!scheduler.canSchedule() && Build.VERSION.SDK_INT >= 31){
Intent().also { intent ->
intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
requireContext().startActivity(intent)
}
allowChange = false
}else{
scheduler.schedule()
}
}else{
scheduler.cancel()
//start worker if there are leftover downloads waiting for scheduler
val workConstraints = Constraints.Builder()
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")
.setConstraints(workConstraints.build())
.setInitialDelay(1000L, TimeUnit.MILLISECONDS)
WorkManager.getInstance(requireContext()).enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
allowChange
}
scheduleStart?.setOnPreferenceClickListener {
UiUtil.showTimePicker(parentFragmentManager, preferences){
val hr = it.get(Calendar.HOUR_OF_DAY)
val mn = it.get(Calendar.MINUTE)
val formattedTime = String.format("%02d", hr) + ":" + String.format("%02d", mn)
preferences.edit().putString("schedule_start",formattedTime).apply()
scheduleStart.summary = formattedTime
scheduler.schedule()
}
true
}
scheduleEnd?.setOnPreferenceClickListener {
UiUtil.showTimePicker(parentFragmentManager, preferences){
val hr = it.get(Calendar.HOUR_OF_DAY)
val mn = it.get(Calendar.MINUTE)
val formattedTime = String.format("%02d", hr) + ":" + String.format("%02d", mn)
preferences.edit().putString("schedule_end",formattedTime).apply()
scheduleEnd.summary = formattedTime
scheduler.schedule()
}
true
}
findPreference<EditTextPreference>("proxy")?.apply {
val s = getString(R.string.socks5_proxy_summary)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${newValue}]"
}
true
}
}
findPreference<ListPreference>("preferred_download_type")?.apply {
val s = getString(R.string.preferred_download_type_summary)
summary = if (value.isNullOrBlank()) {
s
}else {
"${s}\n[${entries[entryValues.indexOf(value)]}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${entries[entryValues.indexOf(newValue)]}]"
}
true
}
}
findPreference<EditTextPreference>("limit_rate")?.apply {
val s = getString(R.string.limit_rate_summary)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${newValue}]"
}
true
}
}
findPreference<EditTextPreference>("buffer_size")?.apply {
val s = getString(R.string.buffer_size_summary)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${newValue}]"
}
true
}
}
findPreference<EditTextPreference>("socket_timeout")?.apply {
val s = getString(R.string.socket_timeout_description)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${newValue}]"
}
true
}
}
findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) {
resetPreferences(preferences.edit(), R.xml.downloading_preferences)
requireActivity().recreate()
val fragmentId = findNavController().currentDestination?.id
findNavController().popBackStack(fragmentId!!,true)
findNavController().navigate(fragmentId)
}
true
}
}
private var archivePathResultLauncher = 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
)
}
val path = result.data!!.data.toString()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = preferences.edit()
editor.putString("download_archive_path", path)
editor.apply()
archivePath.summary = FileUtil.getDownloadArchivePath(requireContext())
}
}
}

@ -1,361 +0,0 @@
package com.deniscerri.ytdl.ui.more.settings
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
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.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.MoveCacheFilesWorker
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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 cachePath: Preference? = null
private var accessAllFiles : Preference? = null
private var noFragments: SwitchPreferenceCompat? = null
private var keepFragments: SwitchPreferenceCompat? = null
private var cacheDownloads : Preference? = null
private var audioFilenameTemplate : Preference? = null
private var videoFilenameTemplate : Preference? = null
private var clearCache: Preference? = null
private var moveCache: Preference? = null
private lateinit var preferences: SharedPreferences
private lateinit var editor: SharedPreferences.Editor
private lateinit var downloadViewModel: DownloadViewModel
private var activeDownloadCount = 0
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.folders_preference, rootKey)
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
editor = preferences.edit()
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
musicPath = findPreference("music_path")
videoPath = findPreference("video_path")
commandPath = findPreference("command_path")
cachePath = findPreference("cache_path")
accessAllFiles = findPreference("access_all_files")
noFragments = findPreference("no_part")
keepFragments = findPreference("keep_cache")
cacheDownloads = findPreference("cache_downloads")
videoFilenameTemplate = findPreference("file_name_template")
audioFilenameTemplate = findPreference("file_name_template_audio")
clearCache = findPreference("clear_cache")
moveCache = findPreference("move_cache")
if (preferences.getString("music_path", "")!!.isEmpty()) {
editor.putString("music_path", FileUtil.getDefaultAudioPath()).apply()
}
if (preferences.getString("video_path", "")!!.isEmpty()) {
editor.putString("video_path", FileUtil.getDefaultVideoPath()).apply()
}
if (preferences.getString("command_path", "")!!.isEmpty()) {
editor.putString("command_path", FileUtil.getDefaultCommandPath()).apply()
}
if (preferences.getString("cache_path", "")!!.isEmpty()) {
editor.putString("cache_path", FileUtil.getCachePath(requireContext())).apply()
}
if (FileUtil.hasAllFilesAccess()) {
accessAllFiles!!.isVisible = false
cacheDownloads!!.isEnabled = true
}else{
editor.putBoolean("cache_downloads", true).apply()
cacheDownloads!!.isEnabled = false
}
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
}
cachePath!!.summary = FileUtil.formatPath(preferences.getString("cache_path", FileUtil.getCachePath(requireContext()))!!)
cachePath!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.cache_directory), getString(R.string.cache_directory_warning)) {
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)
cachePathResultLauncher.launch(intent)
}
true
}
if(VERSION.SDK_INT >= 30){
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
}
}
if (noFragments!!.isChecked) {
editor.putBoolean("keep_cache", false).apply()
keepFragments!!.isChecked = false
keepFragments!!.isEnabled = false
}
noFragments!!.setOnPreferenceChangeListener { _, newValue ->
if(newValue as Boolean){
editor.putBoolean("keep_cache", false).apply()
keepFragments!!.isChecked = false
keepFragments!!.isEnabled = false
}else{
keepFragments!!.isEnabled = true
}
true
}
videoFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
videoFilenameTemplate?.summary = preferences.getString("file_name_template", "%(uploader).30B - %(title).170B")
audioFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]"
audioFilenameTemplate?.summary = preferences.getString("file_name_template_audio", "%(uploader).30B - %(title).170B")
videoFilenameTemplate?.setOnPreferenceClickListener {
UiUtil.showFilenameTemplateDialog(requireActivity(),videoFilenameTemplate?.summary.toString() ?: "", "${getString(R.string.file_name_template)} [${getString(R.string.video)}]") {
editor.putString("file_name_template", it).apply()
videoFilenameTemplate?.summary = it
}
false
}
audioFilenameTemplate?.setOnPreferenceClickListener {
UiUtil.showFilenameTemplateDialog(requireActivity(), audioFilenameTemplate?.summary.toString() ?: "", "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]") {
editor.putString("file_name_template_audio", it).apply()
audioFilenameTemplate?.summary = it
}
false
}
var cacheSize = File(FileUtil.getCachePath(requireContext())).walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
val filesize = if (cacheSize < 10000) {
"0B"
}else {
FileUtil.convertFileSize(cacheSize)
}
clearCache!!.summary = "${resources.getString(R.string.clear_temporary_files_summary)} (${filesize}) "
clearCache!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
lifecycleScope.launch {
activeDownloadCount = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloadsCount()
}
if (activeDownloadCount == 0){
fun clearCacheFolder(folder: File) {
if (folder.exists() && folder.isDirectory) {
folder.listFiles()?.forEach { file ->
if (file.isDirectory) {
clearCacheFolder(file)
file.delete()
} else {
file.delete()
}
}
}
}
clearCacheFolder(File(FileUtil.getCachePath(requireContext())))
Snackbar.make(requireView(), getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show()
cacheSize = File(FileUtil.getCachePath(requireContext())).walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
val filesize = if (cacheSize < 10000) {
"0B"
}else {
FileUtil.convertFileSize(cacheSize)
}
clearCache!!.summary = "${resources.getString(R.string.clear_temporary_files_summary)} (${filesize})"
}else{
Snackbar.make(requireView(), getString(R.string.downloads_running_try_later), Snackbar.LENGTH_SHORT).show()
}
}
true
}
moveCache!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val workRequest = OneTimeWorkRequestBuilder<MoveCacheFilesWorker>()
.addTag("cacheFiles")
.build()
WorkManager.getInstance(requireContext()).beginUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.KEEP,
workRequest
).enqueue()
WorkManager.getInstance(requireContext())
.getWorkInfosByTagLiveData("cacheFiles")
.observe(viewLifecycleOwner){ list ->
if (list == null) return@observe
if (list.first() == null) return@observe
if (list.first().state == WorkInfo.State.SUCCEEDED){
cacheSize = File(FileUtil.getCachePath(requireContext())).walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
clearCache!!.summary = "${resources.getString(R.string.clear_temporary_files_summary)} (${FileUtil.convertFileSize(cacheSize)})"
}
}
true
}
findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) {
resetPreferences(editor, R.xml.folders_preference)
requireActivity().recreate()
val fragmentId = findNavController().currentDestination?.id
findNavController().popBackStack(fragmentId!!,true)
findNavController().navigate(fragmentId)
}
true
}
}
override fun onResume() {
if((VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) ||
VERSION.SDK_INT < 30) {
accessAllFiles!!.isVisible = false
cacheDownloads!!.isEnabled = true
}else{
editor.putBoolean("cache_downloads", true).apply()
cacheDownloads!!.isEnabled = false
}
super.onResume()
}
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 var cachePathResultLauncher = 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(cachePath, result.data, CACHE_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)
CACHE_PATH_CODE -> editor.putString("cache_path", path)
}
editor.apply()
}
companion object {
const val MUSIC_PATH_CODE = 33333
const val VIDEO_PATH_CODE = 55555
const val COMMAND_PATH_CODE = 77777
const val CACHE_PATH_CODE = 99999
}
}

@ -1,472 +0,0 @@
package com.deniscerri.ytdl.ui.more.settings
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.util.DisplayMetrics
import android.view.ViewGroup
import android.view.Window
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.databinding.NavOptionsItemBinding
import com.deniscerri.ytdl.ui.adapter.IconsSheetAdapter
import com.deniscerri.ytdl.ui.adapter.NavBarOptionsAdapter
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Locale
class GeneralSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.general
private lateinit var preferences: SharedPreferences
private lateinit var resultViewModel: ResultViewModel
private var updateUtil: UpdateUtil? = null
private var activeDownloadCount = 0
@SuppressLint("BatteryLife")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.general_preferences, rootKey)
NavbarUtil.init(requireContext())
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
updateUtil = UpdateUtil(requireContext())
val editor = preferences.edit()
WorkManager.getInstance(requireContext()).getWorkInfosByTagLiveData("download").observe(this){
activeDownloadCount = 0
it.forEach {w ->
if (w.state == WorkInfo.State.RUNNING) activeDownloadCount++
}
}
findPreference<ListPreference>("app_language")?.apply {
value = Locale.getDefault().language
summary = Locale.getDefault().displayLanguage
setOnPreferenceChangeListener { _, newValue ->
if (newValue == "system") {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(null))
}else{
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(newValue.toString()))
}
summary = Locale.getDefault().displayLanguage
true
}
}
findPreference<Preference>("label_visibility")?.apply {
isVisible = !resources.getBoolean(R.bool.uses_side_nav)
setOnPreferenceChangeListener { _, _ ->
ThemeUtil.recreateMain()
true
}
}
findPreference<Preference>("navigation_bar")?.apply {
isVisible = !resources.getBoolean(R.bool.uses_side_nav)
if (isVisible) {
summary = NavbarUtil.getNavBarItems(requireContext()).filter { it.isVisible }.map { it.title }.joinToString(", ")
}
setOnPreferenceClickListener {
val binding = requireActivity().layoutInflater.inflate(R.layout.simple_options_recycler, null)
val options = NavbarUtil.getNavBarItems(requireContext())
val optionsRecycler = binding.findViewById<RecyclerView>(R.id.options_recycler)
val adapter : NavBarOptionsAdapter?
val onItemClick = object: NavBarOptionsAdapter.OnItemClickListener {
override fun onNavBarOptionDeselected(item: NavOptionsItemBinding) {
optionsRecycler.findViewHolderForLayoutPosition(0)?.apply {
(this as NavBarOptionsAdapter.NavBarOptionsViewHolder).apply {
this.binding.home.performClick()
}
}
}
}
adapter = NavBarOptionsAdapter(
options.toMutableList(),
NavbarUtil.getStartFragmentId(requireContext()),
onItemClick
)
val itemTouchCallback = object : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
return makeMovementFlags(dragFlags, 0)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val itemToMove = adapter.items[viewHolder.absoluteAdapterPosition]
adapter.items.remove(itemToMove)
adapter.items.add(target.absoluteAdapterPosition, itemToMove)
adapter.notifyItemMoved(
viewHolder.absoluteAdapterPosition,
target.absoluteAdapterPosition
)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// do nothing
}
}
optionsRecycler.layoutManager = LinearLayoutManager(context)
optionsRecycler.adapter = adapter
val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
itemTouchHelper.attachToRecyclerView(optionsRecycler)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.navigation_bar)
.setView(binding)
.setPositiveButton(R.string.ok) { _, _ ->
NavbarUtil.setNavBarItems(adapter.items, requireContext())
NavbarUtil.setStartFragment(adapter.selectedHomeTabId)
summary = adapter.items.filter { it.isVisible }.map { it.title }.joinToString(", ")
ThemeUtil.recreateMain()
}
.setNegativeButton(R.string.cancel, null)
.show()
true
}
}
findPreference<ListPreference>("ytdlnis_theme")?.apply {
summary = entry
setOnPreferenceChangeListener { _, newValue ->
val dialog = MaterialAlertDialogBuilder(context)
dialog.setTitle(context.getString(R.string.app_icon_change))
dialog.setNegativeButton(context.getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
dialog.setPositiveButton(context.getString(R.string.ok)) { _: DialogInterface?, _: Int ->
summary = when(newValue){
"System" -> {
getString(R.string.system)
}
"Dark" -> {
getString(R.string.dark)
}
else -> {
getString(R.string.light)
}
}
editor.putString("ytdlnis_theme", newValue.toString()).apply()
ThemeUtil.updateThemes()
}
dialog.show()
false
}
}
findPreference<Preference>("ytdlnis_icon")?.apply {
val currentValue = preferences.getString("ytdlnis_icon", "default")
IconsSheetAdapter.availableIcons.firstOrNull { it.activityAlias == currentValue }?.let {
summary = getString(it.nameResource)
}
setOnPreferenceClickListener {
val bottomSheet = BottomSheetDialog(context)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.generic_list)
val recycler = bottomSheet.findViewById<RecyclerView>(R.id.download_recyclerview)!!
recycler.layoutManager = GridLayoutManager(context, 3)
recycler.adapter = IconsSheetAdapter(requireActivity())
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
)
false
}
}
findPreference<ListPreference>("theme_accent")?.apply {
summary = entry
setOnPreferenceChangeListener { _, _ ->
ThemeUtil.updateThemes()
true
}
}
findPreference<SwitchPreferenceCompat>("high_contrast")?.apply {
setOnPreferenceChangeListener { _, _ ->
ThemeUtil.updateThemes()
true
}
}
findPreference<SwitchPreferenceCompat>("show_terminal")?.apply {
setOnPreferenceChangeListener { pref, _ ->
val packageManager = requireContext().packageManager
val aliasComponentName = ComponentName(requireContext(), "com.deniscerri.ytdl.terminalShareAlias")
if ((pref as SwitchPreferenceCompat).isChecked){
packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP)
}else{
packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP)
}
true
}
}
findPreference<SwitchPreferenceCompat>("show_quick_download_share")?.apply {
setOnPreferenceChangeListener { pref, _ ->
val packageManager = requireContext().packageManager
val aliasComponentName = ComponentName(requireContext(), "com.deniscerri.ytdl.quickDownloadShareAlias")
if ((pref as SwitchPreferenceCompat).isChecked){
packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP)
}else{
packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP)
}
true
}
}
findPreference<SwitchPreferenceCompat>("display_over_apps")?.apply {
isChecked = Settings.canDrawOverlays(requireContext())
setOnPreferenceChangeListener { _, _ ->
runCatching {
val i = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + requireContext().packageName)
)
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(i)
displayOverAppsResultLauncher.launch(i)
}
true
}
}
findPreference<Preference>("ignore_battery")?.apply {
setOnPreferenceClickListener {
val intent = Intent()
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:" + requireContext().packageName)
startActivity(intent)
true
}
}
findPreference<MultiSelectListPreference>("hide_thumbnails")?.apply {
values.filter { it.isNotBlank() }.apply {
summary = joinToString(", ") { entries[entryValues.indexOf(it)] }
}
setOnPreferenceChangeListener { _, newValues ->
(newValues as Set<*>).map { it as String }.filter { it.isNotBlank() }.apply {
summary = joinToString(", ") { entries[entryValues.indexOf(it)] }
}
true
}
}
findPreference<MultiSelectListPreference>("modify_download_card")?.apply {
values.filter { it.isNotBlank() }.apply {
summary = joinToString(", ") { entries[entryValues.indexOf(it)] }
}
setOnPreferenceChangeListener { _, newValues ->
(newValues as Set<*>).map { it as String }.filter { it.isNotBlank() }.apply {
summary = joinToString(", ") { entries[entryValues.indexOf(it)] }
}
true
}
}
findPreference<ListPreference>("recommendations_home")?.apply {
val s = getString(R.string.video_recommendations_summary)
summary = if (value.isNullOrBlank()) {
s
}else {
"${s}\n[${entries[entryValues.indexOf(value)]}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${entries[entryValues.indexOf(newValue)]}]"
}
findPreference<EditTextPreference>("api_key")?.isVisible = newValue == "yt_api"
findPreference<EditTextPreference>("custom_home_recommendation_url")?.isVisible = newValue == "custom"
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultViewModel.deleteAll()
}
}
true
}
}
findPreference<EditTextPreference>("custom_home_recommendation_url")?.apply {
title = "[${getString(R.string.video_recommendations)}] ${getString(R.string.custom)}"
isVisible = preferences.getString("recommendations_home", "") == "custom"
setOnPreferenceChangeListener { preference, newValue ->
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultViewModel.deleteAll()
}
}
true
}
}
findPreference<EditTextPreference>("api_key")?.apply {
isVisible = preferences.getString("recommendations_home", "") == "yt_api"
val s = getString(R.string.api_key_summary)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${newValue}]"
}
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultViewModel.deleteAll()
}
}
true
}
}
findPreference<ListPreference>("search_engine")?.apply {
val s = getString(R.string.preferred_search_engine_summary)
summary = if (value.isNullOrBlank()) {
s
}else {
"${s}\n[${entries[entryValues.indexOf(value)]}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${entries[entryValues.indexOf(newValue)]}]"
}
true
}
}
findPreference<MultiSelectListPreference>("swipe_gesture")?.apply {
val s = getString(R.string.swipe_gestures_summary)
if (values.size == entries.size) {
summary = "${s}\n[${getString(R.string.all)}]"
}else if (values.size > 0) {
val indexes = entryValues.mapIndexed { index, _ -> index }
summary = "${s}\n[${entries.filterIndexed { index, _ -> indexes.contains(index) }.joinToString(", ")}]"
}else{
summary = s
}
setOnPreferenceChangeListener { _, newValue ->
val newValues = newValue as Set<*>
if (newValues.size == entries.size) {
summary = "${s}\n[${getString(R.string.all)}]"
}else if (newValues.isNotEmpty()) {
val indexes = List(newValues.size) { index -> index }
summary = "${s}\n[${entries.filterIndexed { index, _ -> indexes.contains(index) }.joinToString(", ")}]"
}else{
summary = s
}
true
}
}
findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) {
resetPreferences(editor, R.xml.general_preferences)
ThemeUtil.updateThemes()
val fragmentId = findNavController().currentDestination?.id
findNavController().popBackStack(fragmentId!!,true)
findNavController().navigate(fragmentId)
}
true
}
}
override fun onResume() {
val packageName: String = requireContext().packageName
val pm = requireContext().applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
if (pm.isIgnoringBatteryOptimizations(packageName)) {
findPreference<Preference>("ignore_battery")?.isVisible = false
}
super.onResume()
}
private var displayOverAppsResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { _ ->
findNavController().popBackStack(R.id.appearanceSettingsFragment, false)
}
}

@ -0,0 +1,31 @@
package com.deniscerri.ytdl.ui.more.settings
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.edit
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R
class PreferenceActivityResultDelegate(caller: ActivityResultCaller) {
private var pendingCallback : ((result: ActivityResult) -> Unit)? = null
private val launcher = caller.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
pendingCallback?.invoke(result)
}
}
fun launch(
intent: Intent,
callback: (result: ActivityResult) -> Unit
) {
pendingCallback = callback
launcher.launch(intent)
}
}

@ -1,169 +0,0 @@
package com.deniscerri.ytdl.ui.more.settings
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.navigation.fragment.findNavController
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.util.UiUtil
class ProcessingSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.processing
@SuppressLint("RestrictedApi")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.processing_preferences, rootKey)
val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity())
val editor = prefs.edit()
val preferredFormatID : EditTextPreference? = findPreference("format_id")
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?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]"
preferredFormatIDAudio?.title = "${getString(R.string.preferred_format_id)} [${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(), listOf(), prefs.getString("subs_lang", "en.*,.*-orig")!!){
editor.putString("subs_lang", it)
editor.apply()
subtitleLanguages.summary = it
}
true
}
findPreference<EditTextPreference>("format_id")?.apply {
val s = getString(R.string.preferred_format_id_summary)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${newValue}]"
}
true
}
}
findPreference<EditTextPreference>("format_id_audio")?.apply {
val s = getString(R.string.preferred_format_id_summary)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${newValue}]"
}
true
}
}
findPreference<Preference>("audio_bitrate")?.apply {
var currentValue = prefs.getString("audio_bitrate", "")!!
val entries = context.resources.getStringArray(R.array.audio_bitrate)
val entryValues = context.resources.getStringArray(R.array.audio_bitrate_values)
summary = if (currentValue.isNotBlank()) {
entries[entryValues.indexOf(currentValue)]
}else {
getString(R.string.defaultValue)
}
setOnPreferenceClickListener {
currentValue = prefs.getString("audio_bitrate", "")!!
UiUtil.showAudioBitrateDialog(requireActivity(), currentValue) {
editor.putString("audio_bitrate", it).apply()
summary = if (it.isNotBlank()) {
entries[entryValues.indexOf(it)]
}else {
getString(R.string.defaultValue)
}
}
true
}
}
val audioCodecPref = findPreference<ListPreference>("audio_codec")
val videoCodecPref = findPreference<ListPreference>("video_codec")
val videoContainerPref = findPreference<ListPreference>("video_format")
val recodeVideoPreference = findPreference<SwitchPreferenceCompat>("recode_video")!!
val compatibleVideoPreference = findPreference<SwitchPreferenceCompat>("compatible_video")!!
audioCodecPref?.isEnabled = !compatibleVideoPreference.isChecked
videoCodecPref?.isEnabled = !compatibleVideoPreference.isChecked
videoContainerPref?.isEnabled = !compatibleVideoPreference.isChecked
recodeVideoPreference.setOnPreferenceClickListener {
if (compatibleVideoPreference.isChecked && recodeVideoPreference.isChecked) {
compatibleVideoPreference.performClick()
}
true
}
compatibleVideoPreference.setOnPreferenceClickListener {
audioCodecPref?.isEnabled = !compatibleVideoPreference.isChecked
videoCodecPref?.isEnabled = !compatibleVideoPreference.isChecked
videoContainerPref?.isEnabled = !compatibleVideoPreference.isChecked
if (compatibleVideoPreference.isChecked) {
if (recodeVideoPreference.isChecked) {
recodeVideoPreference.performClick()
}
editor.putString("audio_codec_tmp", audioCodecPref?.value ?: "").apply()
editor.putString("video_codec_tmp", videoCodecPref?.value ?: "").apply()
editor.putString("video_format_tmp", videoContainerPref?.value ?: "").apply()
val audioCodecs = requireContext().getStringArray(R.array.audio_codec)
val audioCodecValues = requireContext().getStringArray(R.array.audio_codec_values)
val videoCodecs = requireContext().getStringArray(R.array.video_codec)
val videoCodecValues = requireContext().getStringArray(R.array.video_codec_values)
val newAudioCodec = "M4A"
val newVideoCodec = "AVC (H264)"
editor.putString("audio_codec", audioCodecValues[audioCodecs.indexOf(newAudioCodec)]).apply()
editor.putString("video_codec", videoCodecValues[videoCodecs.indexOf(newVideoCodec)]).apply()
editor.putString("video_format", "").apply()
requireActivity().recreate()
} else {
editor.putString("audio_codec", prefs.getString("audio_codec_tmp", "")).apply()
editor.putString("video_codec", prefs.getString("video_codec_tmp", "")).apply()
editor.putString("video_format", prefs.getString("video_format_tmp", "")).apply()
requireActivity().recreate()
}
true
}
findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) {
resetPreferences(editor, R.xml.processing_preferences)
requireActivity().recreate()
val fragmentId = findNavController().currentDestination?.id
findNavController().popBackStack(fragmentId!!,true)
findNavController().navigate(fragmentId)
}
true
}
}
}

@ -0,0 +1,21 @@
package com.deniscerri.ytdl.ui.more.settings
import android.app.Activity
import android.view.View
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStoreOwner
import androidx.preference.Preference
interface SettingHost {
fun findPref(key: String): Preference?
fun refreshUI()
fun getHostContext(): Activity
val hostLifecycleOwner: LifecycleOwner
val hostViewModelStoreOwner: ViewModelStoreOwner
val activityResultDelegate: PreferenceActivityResultDelegate
val hostView: View?
fun requestGetParentFragmentManager(): FragmentManager
fun requestRecreateActivity()
fun requestNavigate(id: Int)
}

@ -0,0 +1,7 @@
package com.deniscerri.ytdl.ui.more.settings
import androidx.preference.Preference
interface SettingModule {
fun bindLogic(pref: Preference, host: SettingHost)
}

@ -1,18 +1,74 @@
package com.deniscerri.ytdl.ui.more.settings
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.activity.addCallback
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.recyclerview.widget.LinearLayoutManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.SearchSettingsItem
import com.deniscerri.ytdl.databinding.ActivitySettingsBinding
import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.ui.more.settings.search.SettingsSearchAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.lazy
class SettingsActivity : BaseActivity() {
class SettingsActivity : BaseActivity(), SettingHost {
var context: Context? = null
private lateinit var navController: NavController
private lateinit var searchAdapter: SettingsSearchAdapter
private var allIndexedItems = listOf<SearchSettingsItem>()
override fun findPref(key: String): Preference? {
return allIndexedItems.find { it.preference.key == key }?.preference
}
@SuppressLint("NotifyDataSetChanged")
override fun refreshUI() {
binding.searchSuggestionsRecycler.post {
if (!isFinishing && !isDestroyed) {
searchAdapter.notifyDataSetChanged()
}
}
}
override fun getHostContext() = this
override val activityResultDelegate = PreferenceActivityResultDelegate(this)
override val hostViewModelStoreOwner by lazy {
this
}
override val hostLifecycleOwner by lazy {
this
}
override val hostView: View? by lazy {
this.findViewById<View>(android.R.id.content)
}
override fun requestGetParentFragmentManager() = supportFragmentManager
override fun requestRecreateActivity() = this.recreate()
override fun requestNavigate(id: Int) {
closeSearchView()
navController.navigate(id)
}
private val xmlToNavId = mapOf(
R.xml.general_preferences to R.id.appearanceSettingsFragment,
R.xml.folders_preference to R.id.folderSettingsFragment,
R.xml.downloading_preferences to R.id.downloadSettingsFragment,
R.xml.processing_preferences to R.id.processingSettingsFragment,
R.xml.updating_preferences to R.id.updateSettingsFragment,
R.xml.advanced_preferences to R.id.advancedSettingsFragment,
)
fun getDestinationIdForXml(xmlRes: Int): Int = xmlToNavId[xmlRes] ?: R.id.mainSettingsFragment
lateinit var binding: ActivitySettingsBinding
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -21,11 +77,12 @@ class SettingsActivity : BaseActivity() {
setContentView(binding.root)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.frame_layout) as NavHostFragment
val navController = navHostFragment.findNavController()
navController = navHostFragment.findNavController()
val listener = NavController.OnDestinationChangedListener { controller, destination, arguments ->
if (destination.id == R.id.mainSettingsFragment){
changeTopAppbarTitle(getString(R.string.settings))
changeTopAppbarTitle(getString(R.string.settings), false)
indexSettings()
}
}
@ -44,9 +101,109 @@ class SettingsActivity : BaseActivity() {
}
if (savedInstanceState == null) navController.navigate(R.id.mainSettingsFragment)
//setup search
val appBar = binding.appBar
val searchBar = binding.searchBar
val toolbar = binding.settingsToolbar
val searchView = binding.searchView
searchView.setupWithSearchBar(searchBar)
appBar.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
if (binding.collapsingToolbar.title != getString(R.string.settings)) return@addOnOffsetChangedListener
val totalScrollRange = appBarLayout.totalScrollRange
// Avoid division by zero
if (totalScrollRange == 0) return@addOnOffsetChangedListener
val percentage = Math.abs(verticalOffset).toFloat() / totalScrollRange
// 1. Handle Menu Icon Visibility
val isCollapsed = percentage > 0.8f
toolbar.menu.findItem(R.id.search)?.isVisible = isCollapsed
// 2. Handle SearchBar Visibility and Space
if (isCollapsed) {
if (searchBar.visibility != View.GONE) {
searchBar.visibility = View.GONE
}
} else {
if (searchBar.visibility != View.VISIBLE) {
searchBar.visibility = View.VISIBLE
}
// Fade it out as we scroll up
searchBar.alpha = 1f - (percentage * 1.2f).coerceAtMost(1f)
}
}
toolbar.setOnMenuItemClickListener { menuItem ->
if (menuItem.itemId == R.id.search) {
searchView.show()
true
} else false
}
indexSettings {
val savedSearch = intent.getStringExtra("search_query")
if (!savedSearch.isNullOrBlank()) {
binding.searchBar.performClick()
filterSettings(savedSearch)
}
}
searchAdapter = SettingsSearchAdapter(emptyList(), this)
binding.searchSuggestionsRecycler.layoutManager = LinearLayoutManager(context)
binding.searchSuggestionsRecycler.adapter = searchAdapter
binding.searchSuggestionsRecycler.itemAnimator = null
binding.searchView.editText.addTextChangedListener { text ->
filterSettings(text.toString())
}
}
override fun onResume() {
refreshUI()
super.onResume()
}
private fun indexSettings(cb: (() -> Unit)? = null) {
lifecycleScope.launch(Dispatchers.IO) {
val indexedItems = SettingsRegistry.indexAll(this@SettingsActivity)
withContext(Dispatchers.Main) {
allIndexedItems = indexedItems
allIndexedItems.forEach { item ->
if (!item.isHeader) {
item.module?.bindLogic(item.preference, this@SettingsActivity)
}
cb?.invoke()
}
}
}
}
private fun filterSettings(query: String) {
intent.putExtra("search_query", query)
if (query.isBlank()) {
searchAdapter.updateList(emptyList())
return
}
val filtered = allIndexedItems.filter { item ->
val titleMatch = item.preference.title?.toString()?.contains(query, ignoreCase = true) == true
val summaryMatch = item.preference.summary?.toString()?.contains(query, ignoreCase = true) == true
val groupMatch = item.groupTitle?.contains(query, ignoreCase = true) == true
item.preference.isVisible && (titleMatch || summaryMatch || groupMatch)
}
searchAdapter.updateList(filtered)
}
fun closeSearchView() {
binding.searchView.hide()
}
fun changeTopAppbarTitle(text: String) {
fun changeTopAppbarTitle(text: String, hideSearch: Boolean = true) {
if (this::binding.isInitialized) binding.collapsingToolbar.title = text
binding.searchBar.isVisible = !hideSearch
binding.settingsToolbar.menu.findItem(R.id.search)?.isVisible = !binding.searchBar.isVisible && !hideSearch
}
}

@ -0,0 +1,85 @@
package com.deniscerri.ytdl.ui.more.settings
import android.content.Context
import androidx.preference.Preference
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.SearchSettingsItem
import com.deniscerri.ytdl.ui.more.settings.advanced.AdvancedSettingsModule
import com.deniscerri.ytdl.ui.more.settings.downloading.DownloadSettingsModule
import com.deniscerri.ytdl.ui.more.settings.folder.FolderSettingsModule
import com.deniscerri.ytdl.ui.more.settings.general.GeneralSettingsModule
import com.deniscerri.ytdl.ui.more.settings.processing.ProcessingSettingsModule
import com.deniscerri.ytdl.ui.more.settings.updating.UpdateSettingsModule
object SettingsRegistry {
private val xmlToModule = mapOf(
R.xml.general_preferences to GeneralSettingsModule,
R.xml.folders_preference to FolderSettingsModule,
R.xml.downloading_preferences to DownloadSettingsModule,
R.xml.processing_preferences to ProcessingSettingsModule,
R.xml.updating_preferences to UpdateSettingsModule,
R.xml.advanced_preferences to AdvancedSettingsModule
)
fun getModuleForXml(xmlRes: Int) = xmlToModule[xmlRes]
fun bindFragment(fragment: BaseSettingsFragment, xmlRes: Int) {
val module = getModuleForXml(xmlRes) ?: return
val allPrefs = mutableListOf<Preference>()
fragment.getPreferences(fragment.preferenceScreen, allPrefs).forEach {
module.bindLogic(it, fragment)
}
}
fun indexAll(context: Context): List<SearchSettingsItem> {
val manager = PreferenceManager(context)
val results = mutableListOf<SearchSettingsItem>()
xmlToModule.forEach { (xmlRes, module) ->
val screen = manager.inflateFromResource(context, xmlRes, null)
results.addAll(crawl(screen, xmlRes, module, null))
}
return results
}
private fun crawl(
group: PreferenceGroup,
xmlId: Int,
module: SettingModule?,
parentTitle: String? = null
): List<SearchSettingsItem> {
val list = mutableListOf<SearchSettingsItem>()
if (!group.title.isNullOrBlank()) {
list.add(SearchSettingsItem(
preference = group,
xmlId = xmlId,
module = module,
groupTitle = group.title.toString(),
isHeader = true,
canRebind = true
))
}
val preferencesWithoutRebindingLogic = listOf("ytdl-version")
for (i in 0 until group.preferenceCount) {
val p = group.getPreference(i)
if (p is PreferenceGroup) {
list.addAll(crawl(p, xmlId, module, group.title?.toString()))
} else if (p.key != null && p.key != "reset_preferences") {
list.add(SearchSettingsItem(
preference = p,
xmlId = xmlId,
module = module,
groupTitle = group.title?.toString() ?: parentTitle,
isHeader = false,
canRebind = !preferencesWithoutRebindingLogic.contains(p.key)
))
}
}
return list
}
}

@ -15,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.ui.adapter.SortableTextItemAdapter
import com.deniscerri.ytdl.ui.more.settings.BaseSettingsFragment
import com.deniscerri.ytdl.ui.more.settings.SettingsRegistry
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -23,70 +24,16 @@ class AdvancedSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.advanced
@SuppressLint("RestrictedApi")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.advanced_preferences, rootKey)
val preferenceXMLRes = R.xml.advanced_preferences
setPreferencesFromResource(preferenceXMLRes, rootKey)
SettingsRegistry.bindFragment(this, preferenceXMLRes)
val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity())
val editor = prefs.edit()
findPreference<Preference>("yt_player_client")?.setOnPreferenceClickListener {
findNavController().navigate(R.id.youtubePlayerClientFragment)
false
}
findPreference<Preference>("generate_po_tokens")?.setOnPreferenceClickListener {
findNavController().navigate(R.id.generateYoutubePoTokensFragment)
false
}
val formatImportanceAudio: Preference? = findPreference("format_importance_audio")
val formatImportanceVideo: Preference? = findPreference("format_importance_video")
formatImportanceAudio?.apply {
title = "${getString(R.string.format_importance)} [${getString(R.string.audio)}]"
val items = requireContext().resources.getStringArray(R.array.format_importance_audio)
val itemValues = requireContext().resources.getStringArray(R.array.format_importance_audio_values).toSet()
val prefVideo = prefs.getString("format_importance_audio", itemValues.joinToString(","))!!
summary = prefVideo.split(",").mapIndexed { index, s -> "${index + 1}. ${items[itemValues.indexOf(s)]}" }.joinToString("\n")
setOnPreferenceClickListener {
val pref = prefs.getString("format_importance_audio", itemValues.joinToString(","))!!
val prefArr = pref.split(",")
val itms = itemValues.sortedBy { prefArr.indexOf(it) }.map {
Pair(it, items[itemValues.indexOf(it)])
}.toMutableList()
showFormatImportanceDialog(title.toString(), itms) { new ->
editor.putString("format_importance_audio", new.joinToString(",") { it.first }).apply()
formatImportanceAudio.summary = new.map { it.second }.mapIndexed { index, s -> "${index + 1}. $s" }.joinToString("\n")
}
true
}
}
formatImportanceVideo?.apply {
title = "${getString(R.string.format_importance)} [${getString(R.string.video)}]"
val items = requireContext().resources.getStringArray(R.array.format_importance_video)
val itemValues = requireContext().resources.getStringArray(R.array.format_importance_video_values).toSet()
val prefVideo = prefs.getString("format_importance_video", itemValues.joinToString(","))!!
summary = prefVideo.split(",").mapIndexed { index, s -> "${index + 1}. ${items[itemValues.indexOf(s)]}" }.joinToString("\n")
setOnPreferenceClickListener {
val pref = prefs.getString("format_importance_video", itemValues.joinToString(","))!!
val prefArr = pref.split(",")
val itms = itemValues.sortedBy { prefArr.indexOf(it) }.map {
Pair(it, items[itemValues.indexOf(it)])
}.toMutableList()
showFormatImportanceDialog(title.toString(), itms) {new ->
editor.putString("format_importance_video", new.joinToString(",") { it.first }).apply()
formatImportanceVideo.summary = new.map { it.second }.mapIndexed { index, s -> "${index + 1}. $s" }.joinToString("\n")
}
true
}
}
findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) {
resetPreferences(editor, R.xml.downloading_preferences)
resetPreferences(editor, preferenceXMLRes)
requireActivity().recreate()
val fragmentId = findNavController().currentDestination?.id
findNavController().popBackStack(fragmentId!!,true)
@ -96,76 +43,4 @@ class AdvancedSettingsFragment : BaseSettingsFragment() {
}
}
private fun showFormatImportanceDialog(t: String, items: MutableList<Pair<String, String>>, onChange: (items: List<Pair<String, String>>) -> Unit){
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(t)
val adapter = SortableTextItemAdapter(items)
val itemTouchCallback = object : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
return makeMovementFlags(dragFlags, 0)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val itemToMove = adapter.items[viewHolder.absoluteAdapterPosition]
adapter.items.remove(itemToMove)
adapter.items.add(target.absoluteAdapterPosition, itemToMove)
adapter.notifyItemMoved(
viewHolder.absoluteAdapterPosition,
target.absoluteAdapterPosition
)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// do nothing
}
}
val linear = LinearLayout(requireActivity())
linear.orientation = LinearLayout.VERTICAL
val note = TextView(requireActivity())
note.text = getString(R.string.format_importance_note)
note.textSize = 16f
note.setTypeface(note.typeface, Typeface.BOLD)
note.setPadding(20,20,20,20)
linear.addView(note)
val recycler = RecyclerView(requireContext())
recycler.layoutManager = LinearLayoutManager(requireContext())
recycler.adapter = adapter
linear.addView(recycler)
val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
itemTouchHelper.attachToRecyclerView(recycler)
builder.setView(linear)
builder.setPositiveButton(
getString(android.R.string.ok)
) { _: DialogInterface?, _: Int ->
onChange(adapter.items)
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
dialog.show()
}
}

@ -0,0 +1,163 @@
package com.deniscerri.ytdl.ui.more.settings.advanced
import android.content.Context
import android.content.DialogInterface
import android.graphics.Typeface
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.edit
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.ui.adapter.SortableTextItemAdapter
import com.deniscerri.ytdl.ui.more.settings.SettingModule
import com.deniscerri.ytdl.ui.more.settings.SettingHost
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlin.collections.indexOf
object AdvancedSettingsModule : SettingModule {
override fun bindLogic(pref: Preference, host: SettingHost) {
val context = pref.context
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
when(pref.key) {
"yt_player_client" -> {
pref.setOnPreferenceClickListener {
host.requestNavigate(R.id.youtubePlayerClientFragment)
false
}
}
"generate_po_tokens" -> {
pref.setOnPreferenceClickListener {
host.requestNavigate(R.id.generateYoutubePoTokensFragment)
false
}
}
"format_importance_audio" -> {
pref.apply {
title = "${context.getString(R.string.format_importance)} [${context.getString(R.string.audio)}]"
val items = context.resources.getStringArray(R.array.format_importance_audio)
val itemValues = context.resources.getStringArray(R.array.format_importance_audio_values).toSet()
val prefVideo = prefs.getString("format_importance_audio", itemValues.joinToString(","))!!
summary = prefVideo.split(",").mapIndexed { index, s -> "${index + 1}. ${items[itemValues.indexOf(s)]}" }.joinToString("\n")
setOnPreferenceClickListener {
val prefValue = prefs.getString("format_importance_audio", itemValues.joinToString(","))!!
val prefArr = prefValue.split(",")
val itms = itemValues.sortedBy { prefArr.indexOf(it) }.map {
Pair(it, items[itemValues.indexOf(it)])
}.toMutableList()
showFormatImportanceDialog(context,title.toString(), itms) { new ->
prefs.edit(commit = true) {
putString("format_importance_audio", new.joinToString(",") { it.first })
}
pref.summary = new.map { it.second }.mapIndexed { index, s -> "${index + 1}. $s" }.joinToString("\n")
host.refreshUI()
}
true
}
}
}
"format_importance_video" -> {
pref.apply {
title = "${context.getString(R.string.format_importance)} [${context.getString(R.string.video)}]"
val items = context.resources.getStringArray(R.array.format_importance_video)
val itemValues = context.resources.getStringArray(R.array.format_importance_video_values).toSet()
val prefVideo = prefs.getString("format_importance_video", itemValues.joinToString(","))!!
summary = prefVideo.split(",").mapIndexed { index, s -> "${index + 1}. ${items[itemValues.indexOf(s)]}" }.joinToString("\n")
setOnPreferenceClickListener {
val prefValue = prefs.getString("format_importance_video", itemValues.joinToString(","))!!
val prefArr = prefValue.split(",")
val itms = itemValues.sortedBy { prefArr.indexOf(it) }.map {
Pair(it, items[itemValues.indexOf(it)])
}.toMutableList()
showFormatImportanceDialog(context,title.toString(), itms) {new ->
prefs.edit(commit = true) {
putString("format_importance_video", new.joinToString(",") { it.first })
}
pref.summary = new.map { it.second }.mapIndexed { index, s -> "${index + 1}. $s" }.joinToString("\n")
host.refreshUI()
}
true
}
}
}
}
}
private fun showFormatImportanceDialog(context: Context, t: String, items: MutableList<Pair<String, String>>, onChange: (items: List<Pair<String, String>>) -> Unit){
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(t)
val adapter = SortableTextItemAdapter(items)
val itemTouchCallback = object : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
return makeMovementFlags(dragFlags, 0)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val itemToMove = adapter.items[viewHolder.absoluteAdapterPosition]
adapter.items.remove(itemToMove)
adapter.items.add(target.absoluteAdapterPosition, itemToMove)
adapter.notifyItemMoved(
viewHolder.absoluteAdapterPosition,
target.absoluteAdapterPosition
)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// do nothing
}
}
val linear = LinearLayout(context)
linear.orientation = LinearLayout.VERTICAL
val note = TextView(context)
note.text = context.getString(R.string.format_importance_note)
note.textSize = 16f
note.setTypeface(note.typeface, Typeface.BOLD)
note.setPadding(20,20,20,20)
linear.addView(note)
val recycler = RecyclerView(context)
recycler.layoutManager = LinearLayoutManager(context)
recycler.adapter = adapter
linear.addView(recycler)
val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
itemTouchHelper.attachToRecyclerView(recycler)
builder.setView(linear)
builder.setPositiveButton(
context.getString(android.R.string.ok)
) { _: DialogInterface?, _: Int ->
onChange(adapter.items)
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
val dialog = builder.create()
dialog.show()
}
}

@ -0,0 +1,35 @@
package com.deniscerri.ytdl.ui.more.settings.downloading
import android.os.Bundle
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.ui.more.settings.BaseSettingsFragment
import com.deniscerri.ytdl.ui.more.settings.SettingsRegistry
import com.deniscerri.ytdl.util.UiUtil
class DownloadSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.downloads
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val preferenceXMLRes = R.xml.downloading_preferences
setPreferencesFromResource(preferenceXMLRes, rootKey)
SettingsRegistry.bindFragment(this, preferenceXMLRes)
val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity())
val editor = prefs.edit()
findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) {
resetPreferences(editor, preferenceXMLRes)
requireActivity().recreate()
val fragmentId = findNavController().currentDestination?.id
findNavController().popBackStack(fragmentId!!,true)
findNavController().navigate(fragmentId)
}
true
}
}
}

@ -0,0 +1,282 @@
package com.deniscerri.ytdl.ui.more.settings.downloading
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.ui.more.settings.SettingModule
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.CleanUpLeftoverDownloads
import com.deniscerri.ytdl.work.DownloadWorker
import java.util.Calendar
import java.util.concurrent.TimeUnit
import androidx.core.content.edit
import androidx.preference.EditTextPreference
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.ui.more.settings.SettingHost
object DownloadSettingsModule : SettingModule {
override fun bindLogic(pref: Preference, host: SettingHost) {
val context = pref.context
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
when(pref.key) {
"remember_download_type" -> {
val rememberDownloadType = pref as SwitchPreferenceCompat
val downloadType = host.findPref("preferred_download_type")
downloadType?.isEnabled = !rememberDownloadType.isChecked
rememberDownloadType.setOnPreferenceChangeListener { _, newValue ->
downloadType?.isEnabled = !(newValue as Boolean)
host.refreshUI()
true
}
}
"prevent_duplicate_downloads" -> {
val archivePath = host.findPref("download_archive_path")
pref.setOnPreferenceChangeListener { _, newValue ->
archivePath?.isVisible = newValue == "download_archive"
host.refreshUI()
true
}
}
"download_archive_path" -> {
pref.summary = FileUtil.getDownloadArchivePath(context)
pref.isVisible = preferences.getString("prevent_duplicate_downloads", "") == "download_archive"
pref.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)
host.activityResultDelegate.launch(intent) { result ->
result.data?.data?.let {
host.getHostContext().contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
val path = result.data!!.data.toString()
preferences.edit(commit = true) {
putString("download_archive_path", path)
}
host.refreshUI()
}
true
}
}
"cleanup_leftover_downloads" -> {
val workManager = WorkManager.getInstance(context)
pref.setOnPreferenceChangeListener { preference, newValue ->
var nextTime : Calendar? = Calendar.getInstance()
when(newValue) {
"daily" -> nextTime?.add(Calendar.DAY_OF_WEEK, 1)
"weekly" -> nextTime?.add(Calendar.DAY_OF_WEEK, 7)
"monthly" -> nextTime?.add(Calendar.MONTH, 1)
else -> nextTime = null
}
if (nextTime == null) workManager.cancelAllWorkByTag("cleanup_leftover_downloads")
else {
val workConstraints = Constraints.Builder()
val allowMeteredNetworks = preferences.getBoolean("metered_networks", true)
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
val delay = nextTime.timeInMillis.minus(System.currentTimeMillis())
val workRequest = OneTimeWorkRequestBuilder<CleanUpLeftoverDownloads>()
.addTag("cleanup_leftover_downloads")
.setConstraints(workConstraints.build())
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
workManager.enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
host.refreshUI()
true
}
}
"use_alarm_for_scheduling" -> {
val scheduler = AlarmScheduler(context)
pref.setOnPreferenceChangeListener { preference, newValue ->
var allowChange = true
if (newValue as Boolean){
if (!scheduler.canSchedule() && Build.VERSION.SDK_INT >= 31){
Intent().also { intent ->
intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
context.startActivity(intent)
}
allowChange = false
}
}
host.refreshUI()
allowChange
}
}
"use_scheduler" -> {
val scheduler = AlarmScheduler(context)
val useScheduler = pref as SwitchPreferenceCompat
useScheduler.setOnPreferenceChangeListener { preference, newValue ->
var allowChange = true
if (newValue as Boolean){
if (!scheduler.canSchedule() && Build.VERSION.SDK_INT >= 31){
Intent().also { intent ->
intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
context.startActivity(intent)
}
allowChange = false
}else{
scheduler.schedule()
}
}else{
scheduler.cancel()
//start worker if there are leftover downloads waiting for scheduler
val workConstraints = Constraints.Builder()
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")
.setConstraints(workConstraints.build())
.setInitialDelay(1000L, TimeUnit.MILLISECONDS)
WorkManager.getInstance(context).enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
host.refreshUI()
allowChange
}
}
"schedule_start" -> {
val scheduler = AlarmScheduler(context)
pref.summary = preferences.getString("schedule_start", "00:00")
pref.setOnPreferenceClickListener {
UiUtil.showTimePicker(host.requestGetParentFragmentManager(), preferences){
val hr = it.get(Calendar.HOUR_OF_DAY)
val mn = it.get(Calendar.MINUTE)
val formattedTime = String.format("%02d", hr) + ":" + String.format("%02d", mn)
preferences.edit(commit = true) {
putString("schedule_start", formattedTime)
}
pref.summary = formattedTime
scheduler.schedule()
}
host.refreshUI()
true
}
}
"schedule_end" -> {
val scheduler = AlarmScheduler(context)
pref.summary = preferences.getString("schedule_end", "05:00")
pref.setOnPreferenceClickListener {
UiUtil.showTimePicker(host.requestGetParentFragmentManager(), preferences){
val hr = it.get(Calendar.HOUR_OF_DAY)
val mn = it.get(Calendar.MINUTE)
val formattedTime = String.format("%02d", hr) + ":" + String.format("%02d", mn)
preferences.edit(commit = true) {
putString("schedule_end",formattedTime)
}
pref.summary = formattedTime
scheduler.schedule()
}
host.refreshUI()
true
}
}
"proxy" -> {
(pref as EditTextPreference).apply {
val s = context.getString(R.string.socks5_proxy_summary)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${newValue}]"
}
host.refreshUI()
true
}
}
}
"preferred_download_type" -> {
(pref as ListPreference).apply {
val s = context.getString(R.string.preferred_download_type_summary)
summary = if (value.isNullOrBlank()) {
s
}else {
"${s}\n[${entries[entryValues.indexOf(value)]}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${entries[entryValues.indexOf(newValue)]}]"
}
host.refreshUI()
true
}
}
}
"buffer_size" -> {
(pref as EditTextPreference).apply {
val s = context.getString(R.string.buffer_size_summary)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${newValue}]"
}
host.refreshUI()
true
}
}
}
"socket_timeout" -> {
(pref as EditTextPreference).apply {
val s = context.getString(R.string.socket_timeout_description)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${newValue}]"
}
host.refreshUI()
true
}
}
}
}
}
}

@ -0,0 +1,71 @@
package com.deniscerri.ytdl.ui.more.settings.folder
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
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.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.more.settings.BaseSettingsFragment
import com.deniscerri.ytdl.ui.more.settings.SettingsRegistry
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.MoveCacheFilesWorker
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class FolderSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.directories
private lateinit var editor: SharedPreferences.Editor
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val preferenceXMLRes = R.xml.folders_preference
setPreferencesFromResource(preferenceXMLRes, rootKey)
SettingsRegistry.bindFragment(this, preferenceXMLRes)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
editor = preferences.edit()
findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) {
resetPreferences(editor, preferenceXMLRes)
requireActivity().recreate()
val fragmentId = findNavController().currentDestination?.id
findNavController().popBackStack(fragmentId!!,true)
findNavController().navigate(fragmentId)
}
true
}
}
override fun onResume() {
if((Build.VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) ||
Build.VERSION.SDK_INT < 30) {
findPreference<Preference>("access_all_files")!!.isVisible = false
findPreference<Preference>("cache_downloads")!!.isEnabled = true
}else{
editor.putBoolean("cache_downloads", true).apply()
findPreference<Preference>("cache_downloads")!!.isEnabled = false
}
super.onResume()
}
}

@ -0,0 +1,343 @@
package com.deniscerri.ytdl.ui.more.settings.folder
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Settings
import androidx.core.content.edit
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.more.settings.SettingHost
import com.deniscerri.ytdl.ui.more.settings.SettingModule
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.MoveCacheFilesWorker
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import kotlin.collections.first
object FolderSettingsModule: SettingModule {
const val MUSIC_PATH_CODE = 33333
const val VIDEO_PATH_CODE = 55555
const val COMMAND_PATH_CODE = 77777
const val CACHE_PATH_CODE = 99999
override fun bindLogic(
pref: Preference,
host: SettingHost
) {
val context = pref.context
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
var activeDownloadCount = 0
val downloadViewModel = ViewModelProvider(host.hostViewModelStoreOwner)[DownloadViewModel::class.java]
when(pref.key) {
"music_path" -> {
if (preferences.getString(pref.key, "")!!.isEmpty()) {
preferences.edit(commit = true) {
putString(pref.key, FileUtil.getDefaultAudioPath())
}
}
pref.apply {
summary = FileUtil.formatPath(preferences.getString(pref.key, "")!!)
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)
host.activityResultDelegate.launch(intent) { result ->
result.data?.data?.let {
host.getHostContext().contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(host,pref, result.data, MUSIC_PATH_CODE)
}
true
}
}
}
"video_path" -> {
if (preferences.getString(pref.key, "")!!.isEmpty()) {
preferences.edit(commit = true) {
putString(pref.key, FileUtil.getDefaultVideoPath())
}
}
pref.apply {
summary = FileUtil.formatPath(preferences.getString(pref.key, "")!!)
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)
host.activityResultDelegate.launch(intent) { result ->
result.data?.data?.let {
host.getHostContext().contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(host, pref, result.data, VIDEO_PATH_CODE)
}
true
}
}
}
"command_path" -> {
if (preferences.getString(pref.key, "")!!.isEmpty()) {
preferences.edit(commit = true) {
putString(pref.key, FileUtil.getDefaultCommandPath())
}
}
pref.apply {
summary = FileUtil.formatPath(preferences.getString("command_path", "")!!)
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)
host.activityResultDelegate.launch(intent) { result ->
result.data?.data?.let {
host.getHostContext().contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(host,pref, result.data, COMMAND_PATH_CODE)
}
true
}
}
}
"cache_path" -> {
if (preferences.getString(pref.key, "")!!.isEmpty()) {
preferences.edit(commit = true) {
putString(pref.key, FileUtil.getCachePath(context))
}
}
pref.apply {
summary = FileUtil.formatPath(preferences.getString(pref.key, FileUtil.getCachePath(context))!!)
isEnabled = (Build.VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) ||
Build.VERSION.SDK_INT < 30
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(context, context.getString(R.string.cache_directory), context.getString(
R.string.cache_directory_warning)) {
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)
host.activityResultDelegate.launch(intent) { result ->
result.data?.data?.let {
host.getHostContext().contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
changePath(host, pref, result.data, CACHE_PATH_CODE)
}
}
true
}
}
}
"access_all_files" -> {
pref.apply {
if ((Build.VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) ||
Build.VERSION.SDK_INT < 30) {
isVisible = false
}
if (Build.VERSION.SDK_INT >= 30) {
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = Uri.parse("package:" + context.packageName)
intent.data = uri
host.getHostContext().startActivity(intent)
host.refreshUI()
true
}
}
}
}
"no_part" -> {
(pref as SwitchPreferenceCompat).apply {
setOnPreferenceChangeListener { _, newValue ->
if(newValue as Boolean){
preferences.edit(commit = true) {
putBoolean("keep_cache", false)
}
}
true
}
}
}
"keep_cache" -> {
(pref as SwitchPreferenceCompat).apply {
val noFragments = host.findPref("no_part") as SwitchPreferenceCompat
if (noFragments.isChecked) {
isEnabled = false
isChecked = false
} else {
isEnabled = true
}
}
}
"cache_downloads" -> {
(pref as SwitchPreferenceCompat).apply {
if (FileUtil.hasAllFilesAccess()) {
isEnabled = true
} else {
isEnabled = false
preferences.edit(commit = true) {
putBoolean(pref.key, true)
}
}
}
}
"file_name_template" -> {
pref.apply {
title = "${context.getString(R.string.file_name_template)} [${context.getString(R.string.video)}]"
summary = preferences.getString(pref.key, "%(uploader).30B - %(title).170B")
setOnPreferenceClickListener {
UiUtil.showFilenameTemplateDialog(host.getHostContext(),pref.summary.toString(), "${context.getString(
R.string.file_name_template)} [${context.getString(R.string.video)}]") {
preferences.edit(commit = true) {
putString(pref.key, it)
}
host.refreshUI()
}
false
}
}
}
"file_name_template_audio" -> {
pref.apply {
title = "${context.getString(R.string.file_name_template)} [${context.getString(R.string.audio)}]"
summary = preferences.getString(pref.key, "%(uploader).30B - %(title).170B")
setOnPreferenceClickListener {
UiUtil.showFilenameTemplateDialog(host.getHostContext(), pref.summary.toString(), "${context.getString(
R.string.file_name_template)} [${context.getString(R.string.audio)}]") {
preferences.edit(commit = true) {
putString(pref.key, it)
}
host.refreshUI()
}
false
}
}
}
"clear_cache" -> {
pref.apply {
val cacheSize = File(FileUtil.getCachePath(context)).walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
val filesize = if (cacheSize < 10000) {
"0B"
}else {
FileUtil.convertFileSize(cacheSize)
}
summary = "${context.resources.getString(R.string.clear_temporary_files_summary)} (${filesize}) "
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
host.hostLifecycleOwner.lifecycleScope.launch {
activeDownloadCount = withContext(Dispatchers.IO) {
downloadViewModel.getActiveDownloadsCount()
}
if (activeDownloadCount == 0){
fun clearCacheFolder(folder: File) {
if (folder.exists() && folder.isDirectory) {
folder.listFiles()?.forEach { file ->
if (file.isDirectory) {
clearCacheFolder(file)
file.delete()
} else {
file.delete()
}
}
}
}
clearCacheFolder(File(FileUtil.getCachePath(context)))
Snackbar.make(host.hostView!!, context.getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show()
}else{
Snackbar.make(host.hostView!!, context.getString(R.string.downloads_running_try_later), Snackbar.LENGTH_SHORT).show()
}
host.refreshUI()
}
true
}
}
}
"move_cache" -> {
pref.apply {
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val workRequest = OneTimeWorkRequestBuilder<MoveCacheFilesWorker>()
.addTag("cacheFiles")
.build()
WorkManager.Companion.getInstance(context).beginUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.KEEP,
workRequest
).enqueue()
WorkManager.Companion.getInstance(context)
.getWorkInfosByTagLiveData("cacheFiles")
.observe(host.hostLifecycleOwner){ list ->
if (list == null) return@observe
if (list.first() == null) return@observe
if (list.first().state == WorkInfo.State.SUCCEEDED){
host.refreshUI()
}
}
true
}
}
}
}
}
private fun changePath(host: SettingHost, p: Preference?, data: Intent?, requestCode: Int) {
val path = data!!.data.toString()
p!!.summary = FileUtil.formatPath(data.data.toString())
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(host.getHostContext())
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)
CACHE_PATH_CODE -> editor.putString("cache_path", path)
}
editor.apply()
host.refreshUI()
}
}

@ -0,0 +1,48 @@
package com.deniscerri.ytdl.ui.more.settings.general
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.os.PowerManager
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.ui.more.settings.BaseSettingsFragment
import com.deniscerri.ytdl.ui.more.settings.SettingsRegistry
import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil
class GeneralSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.general
private lateinit var preferences: SharedPreferences
@SuppressLint("BatteryLife")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val preferenceXMLRes = R.xml.general_preferences
setPreferencesFromResource(preferenceXMLRes, rootKey)
SettingsRegistry.bindFragment(this, preferenceXMLRes)
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = preferences.edit()
findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) {
resetPreferences(editor, preferenceXMLRes)
ThemeUtil.updateThemes()
val fragmentId = findNavController().currentDestination?.id
findNavController().popBackStack(fragmentId!!,true)
findNavController().navigate(fragmentId)
}
true
}
}
override fun onResume() {
val packageName: String = requireContext().packageName
val pm = requireContext().applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
if (pm.isIgnoringBatteryOptimizations(packageName)) {
findPreference<Preference>("ignore_battery")?.isVisible = false
}
super.onResume()
}
}

@ -0,0 +1,450 @@
package com.deniscerri.ytdl.ui.more.settings.general
import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import android.util.DisplayMetrics
import android.view.ViewGroup
import android.view.Window
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.databinding.NavOptionsItemBinding
import com.deniscerri.ytdl.ui.adapter.IconsSheetAdapter
import com.deniscerri.ytdl.ui.adapter.NavBarOptionsAdapter
import com.deniscerri.ytdl.ui.more.settings.SettingHost
import com.deniscerri.ytdl.ui.more.settings.SettingModule
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.ThemeUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Locale
import kotlin.collections.forEach
object GeneralSettingsModule : SettingModule {
override fun bindLogic(
pref: Preference,
host: SettingHost
) {
val context = pref.context
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
var activeDownloadCount = 0
WorkManager.getInstance(context).getWorkInfosByTagLiveData("download").observe(host.hostLifecycleOwner){
activeDownloadCount = 0
it.forEach {w ->
if (w.state == WorkInfo.State.RUNNING) activeDownloadCount++
}
}
val resultViewModel = ViewModelProvider(host.hostViewModelStoreOwner)[ResultViewModel::class.java]
when(pref.key) {
"app_language" -> {
(pref as ListPreference).apply {
value = Locale.getDefault().language
summary = Locale.getDefault().displayLanguage
setOnPreferenceChangeListener { _, newValue ->
if (newValue == "system") {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(null))
}else{
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(newValue.toString()))
}
summary = Locale.getDefault().displayLanguage
host.refreshUI()
true
}
}
}
"label_visibility" -> {
pref.apply {
isVisible = !context.resources.getBoolean(R.bool.uses_side_nav)
setOnPreferenceChangeListener { _, _ ->
ThemeUtil.recreateMain()
host.refreshUI()
true
}
}
}
"navigation_bar" -> {
pref.apply {
NavbarUtil.init(context)
isVisible = !context.resources.getBoolean(R.bool.uses_side_nav)
if (isVisible) {
summary = NavbarUtil.getNavBarItems(context).filter { it.isVisible }.map { it.title }.joinToString(", ")
}
setOnPreferenceClickListener {
val binding = host.getHostContext().layoutInflater.inflate(R.layout.simple_options_recycler, null)
val options = NavbarUtil.getNavBarItems(context)
val optionsRecycler = binding.findViewById<RecyclerView>(R.id.options_recycler)
val adapter : NavBarOptionsAdapter?
val onItemClick = object: NavBarOptionsAdapter.OnItemClickListener {
override fun onNavBarOptionDeselected(item: NavOptionsItemBinding) {
optionsRecycler.findViewHolderForLayoutPosition(0)?.apply {
(this as NavBarOptionsAdapter.NavBarOptionsViewHolder).apply {
this.binding.home.performClick()
}
}
}
}
adapter = NavBarOptionsAdapter(
options.toMutableList(),
NavbarUtil.getStartFragmentId(context),
onItemClick
)
val itemTouchCallback = object : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
return makeMovementFlags(dragFlags, 0)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val itemToMove = adapter.items[viewHolder.absoluteAdapterPosition]
adapter.items.remove(itemToMove)
adapter.items.add(target.absoluteAdapterPosition, itemToMove)
adapter.notifyItemMoved(
viewHolder.absoluteAdapterPosition,
target.absoluteAdapterPosition
)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// do nothing
}
}
optionsRecycler.layoutManager = LinearLayoutManager(context)
optionsRecycler.adapter = adapter
val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
itemTouchHelper.attachToRecyclerView(optionsRecycler)
MaterialAlertDialogBuilder(context)
.setTitle(R.string.navigation_bar)
.setView(binding)
.setPositiveButton(R.string.ok) { _, _ ->
NavbarUtil.setNavBarItems(adapter.items, context)
NavbarUtil.setStartFragment(adapter.selectedHomeTabId)
summary = adapter.items.filter { it.isVisible }.map { it.title }.joinToString(", ")
ThemeUtil.recreateMain()
host.refreshUI()
}
.setNegativeButton(R.string.cancel, null)
.show()
true
}
}
}
"ytdlnis_theme" -> {
(pref as ListPreference).apply {
summary = entry
setOnPreferenceChangeListener { _, newValue ->
val dialog = MaterialAlertDialogBuilder(context)
dialog.setTitle(context.getString(R.string.app_icon_change))
dialog.setNegativeButton(context.getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
dialog.setPositiveButton(context.getString(R.string.ok)) { _: DialogInterface?, _: Int ->
summary = when(newValue){
"System" -> {
context.getString(R.string.system)
}
"Dark" -> {
context.getString(R.string.dark)
}
else -> {
context.getString(R.string.light)
}
}
preferences.edit(commit = true){
putString("ytdlnis_theme", newValue.toString())
}
ThemeUtil.updateThemes()
host.refreshUI()
}
dialog.show()
false
}
}
}
"ytdlnis_icon" -> {
pref.apply {
val currentValue = preferences.getString("ytdlnis_icon", "default")
IconsSheetAdapter.availableIcons.firstOrNull { it.activityAlias == currentValue }?.let {
summary = context.getString(it.nameResource)
}
setOnPreferenceClickListener {
val bottomSheet = BottomSheetDialog(context)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.generic_list)
val recycler = bottomSheet.findViewById<RecyclerView>(R.id.download_recyclerview)!!
recycler.layoutManager = GridLayoutManager(context, 3)
recycler.adapter = IconsSheetAdapter(host)
bottomSheet.show()
val displayMetrics = DisplayMetrics()
host.getHostContext().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
false
}
}
}
"theme_accent" -> {
(pref as ListPreference).apply {
summary = entry
setOnPreferenceChangeListener { _, _ ->
ThemeUtil.updateThemes()
host.refreshUI()
true
}
}
}
"high_contrast" -> {
(pref as SwitchPreferenceCompat).apply {
setOnPreferenceChangeListener { _, _ ->
ThemeUtil.updateThemes()
host.refreshUI()
true
}
}
}
"show_terminal" -> {
(pref as SwitchPreferenceCompat).apply {
setOnPreferenceChangeListener { pref, _ ->
val packageManager = context.packageManager
val aliasComponentName =
ComponentName(context, "com.deniscerri.ytdl.terminalShareAlias")
if ((pref as SwitchPreferenceCompat).isChecked){
packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP)
}else{
packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP)
}
host.refreshUI()
true
}
}
}
"show_quick_download_share" -> {
(pref as SwitchPreferenceCompat).apply {
setOnPreferenceChangeListener { pref, _ ->
val packageManager = context.packageManager
val aliasComponentName =
ComponentName(context, "com.deniscerri.ytdl.quickDownloadShareAlias")
if ((pref as SwitchPreferenceCompat).isChecked){
packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP)
}else{
packageManager.setComponentEnabledSetting(aliasComponentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP)
}
host.refreshUI()
true
}
}
}
"display_over_apps" -> {
(pref as SwitchPreferenceCompat).apply {
isChecked = Settings.canDrawOverlays(context)
setOnPreferenceChangeListener { _, _ ->
runCatching {
val i = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + context.packageName)
)
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
host.getHostContext().startActivity(i)
host.activityResultDelegate.launch(i) {
host.refreshUI()
}
}
true
}
}
}
"ignore_battery" -> {
pref.apply {
setOnPreferenceClickListener {
val intent = Intent()
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:" + context.packageName)
host.getHostContext().startActivity(intent)
true
}
}
}
"hide_thumbnails" -> {
(pref as MultiSelectListPreference).apply {
values.filter { it.isNotBlank() }.apply {
summary = joinToString(", ") { entries[entryValues.indexOf(it)] }
}
setOnPreferenceChangeListener { _, newValues ->
(newValues as Set<*>).map { it as String }.filter { it.isNotBlank() }.apply {
summary = joinToString(", ") { entries[entryValues.indexOf(it)] }
}
host.refreshUI()
true
}
}
}
"modify_download_card" -> {
(pref as MultiSelectListPreference).apply {
values.filter { it.isNotBlank() }.apply {
summary = joinToString(", ") { entries[entryValues.indexOf(it)] }
}
setOnPreferenceChangeListener { _, newValues ->
(newValues as Set<*>).map { it as String }.filter { it.isNotBlank() }.apply {
summary = joinToString(", ") { entries[entryValues.indexOf(it)] }
}
host.refreshUI()
true
}
}
}
"recommendations_home" -> {
(pref as ListPreference).apply {
val s = context.getString(R.string.video_recommendations_summary)
summary = if (value.isNullOrBlank()) {
s
}else {
"${s}\n[${entries[entryValues.indexOf(value)]}]"
}
setOnPreferenceChangeListener { _, newValue ->
host.hostLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
resultViewModel.deleteAll()
}
}
host.refreshUI()
true
}
}
}
"custom_home_recommendation_url" -> {
(pref as EditTextPreference).apply {
title = "[${context.getString(R.string.video_recommendations)}] ${context.getString(R.string.custom)}"
isVisible = preferences.getString("recommendations_home", "") == "custom"
setOnPreferenceChangeListener { preference, newValue ->
host.hostLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
resultViewModel.deleteAll()
}
}
host.refreshUI()
true
}
}
}
"api_key" -> {
(pref as EditTextPreference).apply {
isVisible = preferences.getString("recommendations_home", "") == "yt_api"
val s = context.getString(R.string.api_key_summary)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
host.hostLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
resultViewModel.deleteAll()
}
}
host.refreshUI()
true
}
}
}
"search_engine" -> {
(pref as ListPreference).apply {
val s = context.getString(R.string.preferred_search_engine_summary)
summary = if (value.isNullOrBlank()) {
s
}else {
"${s}\n[${entries[entryValues.indexOf(value)]}]"
}
setOnPreferenceChangeListener { _, newValue ->
host.refreshUI()
true
}
}
}
"swipe_gesture" -> {
(pref as MultiSelectListPreference).apply {
val s = context.getString(R.string.swipe_gestures_summary)
if (values.size == entries.size) {
summary = "${s}\n[${context.getString(R.string.all)}]"
}else if (values.size > 0) {
val indexes = entryValues.mapIndexed { index, _ -> index }
summary = "${s}\n[${entries.filterIndexed { index, _ -> indexes.contains(index) }.joinToString(", ")}]"
}else{
summary = s
}
setOnPreferenceChangeListener { _, newValue ->
val newValues = newValue as Set<*>
if (newValues.size == entries.size) {
summary = "${s}\n[${context.getString(R.string.all)}]"
}else if (newValues.isNotEmpty()) {
val indexes = List(newValues.size) { index -> index }
summary = "${s}\n[${entries.filterIndexed { index, _ -> indexes.contains(index) }.joinToString(", ")}]"
}else{
summary = s
}
host.refreshUI()
true
}
}
}
}
}
}

@ -0,0 +1,39 @@
package com.deniscerri.ytdl.ui.more.settings.processing
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.navigation.fragment.findNavController
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.ui.more.settings.BaseSettingsFragment
import com.deniscerri.ytdl.ui.more.settings.SettingsRegistry
import com.deniscerri.ytdl.util.UiUtil
class ProcessingSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.processing
@SuppressLint("RestrictedApi")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val preferenceXMLRes = R.xml.processing_preferences
setPreferencesFromResource(preferenceXMLRes, rootKey)
SettingsRegistry.bindFragment(this, preferenceXMLRes)
val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity())
val editor = prefs.edit()
findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) {
resetPreferences(editor, preferenceXMLRes)
requireActivity().recreate()
val fragmentId = findNavController().currentDestination?.id
findNavController().popBackStack(fragmentId!!,true)
findNavController().navigate(fragmentId)
}
true
}
}
}

@ -0,0 +1,193 @@
package com.deniscerri.ytdl.ui.more.settings.processing
import androidx.core.content.edit
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.ui.more.settings.SettingModule
import com.deniscerri.ytdl.ui.more.settings.SettingHost
import com.deniscerri.ytdl.util.UiUtil
import kotlin.collections.indexOf
object ProcessingSettingsModule : SettingModule {
override fun bindLogic(
pref: Preference,
host: SettingHost
) {
val context = pref.context
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
when(pref.key) {
"format_id" -> {
(pref as EditTextPreference).apply {
title = "${context.getString(R.string.preferred_format_id)} [${context.getString(R.string.video)}]"
dialogTitle = "${context.getString(R.string.preferred_format_id)} [${context.getString(R.string.video)}]"
val s = context.getString(R.string.preferred_format_id_summary)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${newValue}]"
}
host.refreshUI()
true
}
}
}
"format_id_audio" -> {
(pref as EditTextPreference).apply {
title = "${context.getString(R.string.preferred_format_id)} [${context.getString(R.string.audio)}]"
dialogTitle = "${context.getString(R.string.preferred_format_id)} [${context.getString(R.string.audio)}]"
val s = context.getString(R.string.preferred_format_id_summary)
summary = if (text.isNullOrBlank()) {
s
}else {
"${s}\n[${text}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if ((newValue as String?).isNullOrBlank()) {
s
}else {
"${s}\n[${newValue}]"
}
host.refreshUI()
true
}
}
}
"subs_lang" -> {
pref.apply {
summary = prefs.getString("subs_lang", "en.*,.*-orig")!!
setOnPreferenceClickListener {
UiUtil.showSubtitleLanguagesDialog(host.getHostContext(), listOf(), prefs.getString("subs_lang", "en.*,.*-orig")!!){
prefs.edit(commit = true) {
putString(pref.key, it)
}
summary = it
host.refreshUI()
}
true
}
}
}
"audio_bitrate" -> {
pref.apply {
var currentValue = prefs.getString("audio_bitrate", "")!!
val entries = context.resources.getStringArray(R.array.audio_bitrate)
val entryValues = context.resources.getStringArray(R.array.audio_bitrate_values)
summary = if (currentValue.isNotBlank()) {
entries[entryValues.indexOf(currentValue)]
}else {
context.getString(R.string.defaultValue)
}
setOnPreferenceClickListener {
currentValue = prefs.getString("audio_bitrate", "")!!
UiUtil.showAudioBitrateDialog(host.getHostContext(), currentValue) {
prefs.edit(commit = true) {
putString("audio_bitrate", it)
}
summary = if (it.isNotBlank()) {
entries[entryValues.indexOf(it)]
}else {
context.getString(R.string.defaultValue)
}
host.refreshUI()
}
true
}
}
}
"audio_codec" -> {
updateCompatibleVideoConfig(host)
}
"video_codec" -> {
updateCompatibleVideoConfig(host)
}
"video_format" -> {
updateCompatibleVideoConfig(host)
}
"recode_video" -> {
val compatibleVideoPreference = host.findPref("compatible_video") as SwitchPreferenceCompat
pref.setOnPreferenceClickListener {
if (compatibleVideoPreference.isChecked && (pref as SwitchPreferenceCompat).isChecked) {
compatibleVideoPreference.performClick()
}
true
}
}
"compatible_video" -> {
updateCompatibleVideoConfig(host)
val prefSwitch = pref as SwitchPreferenceCompat
pref.setOnPreferenceClickListener {
if (prefSwitch.isChecked) {
val recodeVideoPreference = host.findPref("recode_video") as SwitchPreferenceCompat
if (recodeVideoPreference.isChecked) {
recodeVideoPreference.performClick()
}
val audioCodecPref = host.findPref("audio_codec") as? ListPreference
val videoCodecPref = host.findPref("video_codec") as? ListPreference
val videoContainerPref = host.findPref("video_format") as? ListPreference
prefs.edit(commit = true) {
putString("audio_codec_tmp", audioCodecPref?.value ?: "")
putString("video_codec_tmp", videoCodecPref?.value ?: "")
putString("video_format_tmp", videoContainerPref?.value ?: "")
}
val audioCodecs = context.getStringArray(R.array.audio_codec)
val audioCodecValues = context.getStringArray(R.array.audio_codec_values)
val videoCodecs = context.getStringArray(R.array.video_codec)
val videoCodecValues = context.getStringArray(R.array.video_codec_values)
val newAudioCodec = "M4A"
val newVideoCodec = "AVC (H264)"
prefs.edit(commit = true) {
putString("audio_codec", audioCodecValues[audioCodecs.indexOf(newAudioCodec)])
putString("video_codec", videoCodecValues[videoCodecs.indexOf(newVideoCodec)])
putString("video_format", "")
}
host.refreshUI()
host.requestRecreateActivity()
} else {
prefs.edit(commit = true) {
putString("audio_codec", prefs.getString("audio_codec_tmp", ""))
putString("video_codec", prefs.getString("video_codec_tmp", ""))
putString("video_format", prefs.getString("video_format_tmp", ""))
}
host.refreshUI()
host.requestRecreateActivity()
}
true
}
}
}
}
fun updateCompatibleVideoConfig(host: SettingHost) {
val audioCodecPref = host.findPref("audio_codec")
val videoCodecPref = host.findPref("video_codec")
val videoContainerPref = host.findPref("video_format")
val compatibleVideoPreference = host.findPref("compatible_video") as SwitchPreferenceCompat
audioCodecPref?.isEnabled = !compatibleVideoPreference.isChecked
videoCodecPref?.isEnabled = !compatibleVideoPreference.isChecked
videoContainerPref?.isEnabled = !compatibleVideoPreference.isChecked
}
}

@ -0,0 +1,223 @@
package com.deniscerri.ytdl.ui.more.settings.search
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.SeekBar
import android.widget.TextView
import androidx.appcompat.widget.SwitchCompat
import androidx.core.view.isVisible
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.preference.ListPreference
import androidx.preference.SeekBarPreference
import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.SearchSettingsItem
import com.deniscerri.ytdl.ui.more.settings.DefaultPreferenceActions
import com.deniscerri.ytdl.ui.more.settings.SettingsActivity
import com.deniscerri.ytdl.ui.more.settings.SettingsRegistry
import com.google.android.material.button.MaterialButton
class SettingsSearchAdapter(
private var items: List<SearchSettingsItem>,
private val activity: SettingsActivity
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@SuppressLint("NotifyDataSetChanged")
fun updateList(newList: List<SearchSettingsItem>) {
items = newList
notifyDataSetChanged()
}
companion object {
private const val TYPE_DEFAULT = 0
private const val TYPE_SWITCH = 1
private const val TYPE_SEEKBAR = 2
private const val TYPE_HEADER = 3
}
override fun getItemViewType(position: Int): Int {
if (items[position].isHeader) return TYPE_HEADER
return when (items[position].preference) {
is SwitchPreferenceCompat -> TYPE_SWITCH
is SeekBarPreference -> TYPE_SEEKBAR
else -> TYPE_DEFAULT
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
TYPE_HEADER -> {
val view = inflater.inflate(R.layout.preference_search_result_title, parent, false)
HeaderViewHolder(view)
}
TYPE_SWITCH -> {
val view = inflater.inflate(R.layout.preference_search_result_switch, parent, false)
SwitchViewHolder(view)
}
TYPE_SEEKBAR -> {
val view = inflater.inflate(R.layout.preference_search_result_seekbar, parent, false)
SeekbarViewHolder(view)
}
else -> {
val view = inflater.inflate(R.layout.preference_search_result_regular, parent, false)
DefaultViewHolder(view)
}
}
}
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: List<Any?>
) {
if (payloads.contains("SKIP_BIND_LOGIC")) {
bindVisualsOnly(holder, position)
} else {
super.onBindViewHolder(holder, position, payloads)
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
bindVisualsOnly(holder, position)
}
private fun bindVisualsOnly(holder: RecyclerView.ViewHolder, position: Int) {
val item = items[position]
val pref = item.preference
if (pref.title.isNullOrBlank())
if (!item.isHeader && item.canRebind) {
item.module?.bindLogic(pref, activity)
}
// Handle Visuals (Dependencies)
holder.itemView.isEnabled = pref.isEnabled
holder.itemView.alpha = if (pref.isEnabled) 1.0f else 0.5f
holder.itemView.setOnLongClickListener {
val bundle = Bundle().apply {
putString("highlight_key", pref.key)
}
val destinationId = activity.getDestinationIdForXml(item.xmlId)
val navHostFragment = activity.supportFragmentManager.findFragmentById(R.id.frame_layout) as NavHostFragment
val navController = navHostFragment.findNavController()
activity.closeSearchView()
navController.navigate(destinationId, bundle)
true
}
// Bind the UI
when (holder) {
is HeaderViewHolder -> {
holder.title.text = item.groupTitle
}
is SwitchViewHolder -> {
holder.switchWidget.setOnCheckedChangeListener(null)
holder.title.text = pref.title
holder.summary.text = pref.summary
holder.summary.isVisible = !pref.summary.isNullOrBlank()
holder.switchWidget.isChecked = (pref as SwitchPreferenceCompat).isChecked
holder.switchWidget.isFocusable = pref.isEnabled
holder.switchWidget.isClickable = pref.isEnabled
holder.icon.isVisible = pref.icon != null
pref.icon?.apply { holder.icon.icon = this }
holder.itemView.setOnClickListener {
holder.switchWidget.performClick()
}
holder.switchWidget.setOnCheckedChangeListener { _, isChecked ->
pref.callChangeListener(isChecked)
pref.isChecked = isChecked
activity.refreshUI()
}
}
is SeekbarViewHolder -> {
holder.title.text = pref.title
holder.summary.text = pref.summary
holder.summary.isVisible = !pref.summary.isNullOrBlank()
holder.icon.isVisible = pref.icon != null
pref.icon?.apply { holder.icon.icon = this }
holder.seekbar.max = (pref as SeekBarPreference).max
holder.seekbar.progress = pref.value
holder.seekbarValue.text = pref.value.toString()
holder.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
holder.seekbarValue.text = progress.toString()
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
seekBar?.apply {
pref.value = progress
pref.callChangeListener(progress)
holder.seekbarValue.text = progress.toString()
activity.refreshUI()
}
}
})
}
is DefaultViewHolder -> {
holder.title.text = pref.title
holder.summary.text = pref.summary
holder.summary.isVisible = !pref.summary.isNullOrBlank()
holder.icon.isVisible = pref.icon != null
pref.icon?.apply { holder.icon.icon = this }
holder.itemView.setOnClickListener {
if (pref.onPreferenceClickListener == null || item.module == null) {
val didLaunchDialog = DefaultPreferenceActions.onPreferenceDisplayDialog( activity, pref) {
activity.refreshUI()
}
if (!didLaunchDialog) {
holder.itemView.performLongClick()
}
} else {
pref.performClick()
}
}
}
}
}
class HeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title: TextView = view.findViewById(R.id.preference_title)
}
class SwitchViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title: TextView = view.findViewById(R.id.preference_title)
val summary: TextView = view.findViewById(R.id.preference_summary)
val switchWidget: SwitchCompat = view.findViewById(R.id.preference_switch)
var icon: MaterialButton = view.findViewById(R.id.preference_icon)
}
class DefaultViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title: TextView = view.findViewById(R.id.preference_title)
val summary: TextView = view.findViewById(R.id.preference_summary)
var icon: MaterialButton = view.findViewById(R.id.preference_icon)
}
class SeekbarViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title: TextView = view.findViewById(R.id.preference_title)
val summary: TextView = view.findViewById(R.id.preference_summary)
var icon: MaterialButton = view.findViewById(R.id.preference_icon)
var seekbar: SeekBar = view.findViewById(R.id.seekBar)
var seekbarValue: TextView = view.findViewById(R.id.seekbarValue)
}
override fun getItemCount() = items.size
}

@ -14,6 +14,7 @@ import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.SettingsViewModel
import com.deniscerri.ytdl.database.viewmodel.YTDLPViewModel
import com.deniscerri.ytdl.ui.more.settings.BaseSettingsFragment
import com.deniscerri.ytdl.ui.more.settings.SettingsRegistry
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil
@ -26,104 +27,17 @@ import java.io.File
class UpdateSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.updating
private var updateYTDL: Preference? = null
private var ytdlVersion: Preference? = null
private var ytdlSource: Preference? = null
private var updateUtil: UpdateUtil? = null
private var version: Preference? = null
private lateinit var preferences: SharedPreferences
private lateinit var ytdlpViewModel: YTDLPViewModel
private lateinit var settingsViewModel: SettingsViewModel
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.updating_preferences, rootKey)
updateUtil = UpdateUtil(requireContext())
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
updateYTDL = findPreference("update_ytdl")
ytdlVersion = findPreference("ytdl-version")
ytdlSource = findPreference("ytdlp_source_label")
ytdlpViewModel = ViewModelProvider(this)[YTDLPViewModel::class.java]
settingsViewModel = ViewModelProvider(this)[SettingsViewModel::class.java]
ytdlSource?.apply {
summary = preferences.getString("ytdlp_source_label", "")!!.ifEmpty { getString(R.string.update_ytdl_stable) }
setOnPreferenceClickListener {
UiUtil.showYTDLSourceBottomSheet(requireActivity(), preferences) { t, r ->
summary = t
preferences.edit().putString("ytdlp_source", r).apply()
preferences.edit().putString("ytdlp_source_label", t).apply()
initYTDLUpdate(r)
}
true
}
}
ytdlVersion?.apply {
lifecycleScope.launch {
summary = getString(R.string.loading)
summary = withContext(Dispatchers.IO){
ytdlpViewModel.getVersion(preferences.getString("ytdlp_source", "stable")!!)
}
if (summary?.isBlank() == true) {
setYTDLPVersion()
}
setOnPreferenceClickListener {
initYTDLUpdate()
true
}
}
}
updateYTDL!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
initYTDLUpdate()
true
}
findPreference<Preference>("changelog")?.setOnPreferenceClickListener {
findNavController().navigate(R.id.changeLogFragment)
false
}
findPreference<Preference>("packages")?.apply {
summary = "Python, FFmpeg, Aria2c, NodeJS"
setOnPreferenceClickListener {
findNavController().navigate(R.id.packagesFragment)
false
}
}
version = findPreference("version")
val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir
version!!.summary = "${BuildConfig.VERSION_NAME} (${nativeLibraryDir?.split("/lib/")?.get(1)})"
version!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
lifecycleScope.launch{
val res = withContext(Dispatchers.IO){
updateUtil!!.tryGetNewVersion()
}
if (res.isFailure) {
Snackbar.make(requireView(), res.exceptionOrNull()?.message ?: getString(R.string.network_error), Snackbar.LENGTH_LONG).show()
}else{
if (preferences.getBoolean("automatic_backup", false)) {
withContext(Dispatchers.IO){
settingsViewModel.backup()
}
}
UiUtil.showNewAppUpdateDialog(res.getOrNull()!!, requireActivity(), preferences)
}
}
true
}
val preferenceXMLRes = R.xml.updating_preferences
setPreferencesFromResource(preferenceXMLRes, rootKey)
SettingsRegistry.bindFragment(this, preferenceXMLRes)
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) {
resetPreferences(preferences.edit(), R.xml.updating_preferences)
resetPreferences(preferences.edit(), preferenceXMLRes)
requireActivity().recreate()
val fragmentId = findNavController().currentDestination?.id
findNavController().popBackStack(fragmentId!!,true)
@ -132,67 +46,4 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
true
}
}
private fun setYTDLPVersion() {
lifecycleScope.launch {
ytdlVersion!!.summary = getString(R.string.loading)
val version = withContext(Dispatchers.IO){
ytdlpViewModel.getVersion(preferences.getString("ytdlp_source", "stable")!!)
}
preferences.edit().apply {
putString("ytdl-version", version)
apply()
}
ytdlVersion!!.summary = version
}
}
private fun initYTDLUpdate(channel: String? = null) = lifecycleScope.launch {
Snackbar.make(requireView(),
requireContext().getString(R.string.ytdl_updating_started),
Snackbar.LENGTH_LONG).show()
runCatching {
val res = updateUtil!!.updateYTDL(channel)
when (res.status) {
UpdateUtil.YTDLPUpdateStatus.DONE -> {
Snackbar.make(requireView(), res.message, Snackbar.LENGTH_LONG).show()
setYTDLPVersion()
val infoJsonPath = FileUtil.getInfoJsonPath(requireContext())
File(infoJsonPath).deleteRecursively()
}
UpdateUtil.YTDLPUpdateStatus.ALREADY_UP_TO_DATE -> Snackbar.make(requireView(),
requireContext().getString(R.string.you_are_in_latest_version),
Snackbar.LENGTH_LONG).show()
UpdateUtil.YTDLPUpdateStatus.ERROR -> {
val msg = res.message
view?.apply {
val snackBar = Snackbar.make(this, msg, Snackbar.LENGTH_LONG)
snackBar.setAction(R.string.copy_log){
UiUtil.copyToClipboard(msg, requireActivity())
}
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()
}
}
else -> {
}
}
}.onFailure {
val msg = it.message ?: requireContext().getString(R.string.errored)
view?.apply {
val snackBar = Snackbar.make(this, msg, Snackbar.LENGTH_LONG)
snackBar.setAction(R.string.copy_log){
UiUtil.copyToClipboard(msg, requireActivity())
}
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()
}
}
}
}

@ -0,0 +1,198 @@
package com.deniscerri.ytdl.ui.more.settings.updating
import android.content.Context
import android.content.SharedPreferences
import android.view.View
import android.widget.TextView
import androidx.core.content.edit
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.SettingsViewModel
import com.deniscerri.ytdl.database.viewmodel.YTDLPViewModel
import com.deniscerri.ytdl.ui.more.settings.SettingModule
import com.deniscerri.ytdl.ui.more.settings.SettingHost
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
object UpdateSettingsModule : SettingModule {
override fun bindLogic(pref: Preference,host: SettingHost) {
val context = pref.context
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val updateUtil = UpdateUtil(context)
val ytdlpViewModel = ViewModelProvider(host.hostViewModelStoreOwner)[YTDLPViewModel::class.java]
val settingsViewModel = ViewModelProvider(host.hostViewModelStoreOwner)[SettingsViewModel::class.java]
when(pref.key) {
"ytdlp_source_label" -> {
pref.apply {
summary = preferences.getString("ytdlp_source_label", "")!!.ifEmpty { context.getString(R.string.update_ytdl_stable) }
setOnPreferenceClickListener {
UiUtil.showYTDLSourceBottomSheet(host.getHostContext(), preferences) { t, r ->
summary = t
preferences.edit().putString("ytdlp_source", r).apply()
preferences.edit().putString("ytdlp_source_label", t).apply()
val ytdlVersionPreference = host.findPref("ytdl-version")!!
initYTDLUpdate(context, host, updateUtil, ytdlpViewModel, preferences, ytdlVersionPreference)
}
true
}
}
}
"ytdl-version" -> {
pref.apply {
host.hostLifecycleOwner.lifecycleScope.launch {
summary = context.getString(R.string.loading)
summary = withContext(Dispatchers.IO){
ytdlpViewModel.getVersion(preferences.getString("ytdlp_source", "stable")!!)
}
if (summary?.isBlank() == true) {
setYTDLPVersion(context, host, ytdlpViewModel, preferences, pref)
}
setOnPreferenceClickListener {
initYTDLUpdate(context, host, updateUtil, ytdlpViewModel, preferences, pref)
true
}
}
}
}
"update_ytdl" -> {
pref.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val ytdlVersionPreference = host.findPref("ytdl-version")!!
initYTDLUpdate(context, host, updateUtil, ytdlpViewModel, preferences, ytdlVersionPreference)
true
}
}
"changelog" -> {
pref.setOnPreferenceClickListener {
host.requestNavigate(R.id.changeLogFragment)
false
}
}
"packages" -> {
pref.apply {
summary = "Python, FFmpeg, Aria2c, NodeJS"
setOnPreferenceClickListener {
host.requestNavigate(R.id.packagesFragment)
false
}
}
}
"version" -> {
pref.apply {
val nativeLibraryDir = context.applicationInfo?.nativeLibraryDir
summary = "${BuildConfig.VERSION_NAME} (${nativeLibraryDir?.split("/lib/")?.get(1)})"
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
host.hostLifecycleOwner.lifecycleScope.launch{
val updateUtil = UpdateUtil(context)
val res = withContext(Dispatchers.IO){
updateUtil.tryGetNewVersion()
}
if (res.isFailure) {
Snackbar.make(host.hostView!!, res.exceptionOrNull()?.message ?: context.getString(R.string.network_error), Snackbar.LENGTH_LONG).show()
}else{
if (preferences.getBoolean("automatic_backup", false)) {
withContext(Dispatchers.IO){
settingsViewModel.backup()
}
}
UiUtil.showNewAppUpdateDialog(res.getOrNull()!!, host.getHostContext(), preferences)
}
}
true
}
}
}
}
}
private fun setYTDLPVersion(
context: Context,
host: SettingHost,
ytdlpViewModel: YTDLPViewModel,
preferences: SharedPreferences,
pref: Preference
) {
host.hostLifecycleOwner.lifecycleScope.launch {
pref.summary = context.getString(R.string.loading)
val version = withContext(Dispatchers.IO){
ytdlpViewModel.getVersion(preferences.getString("ytdlp_source", "stable")!!)
}
preferences.edit(commit = true) {
putString("ytdl-version", version)
}
pref.summary = version
host.refreshUI()
}
}
private fun initYTDLUpdate(
context: Context,
host: SettingHost,
updateUtil: UpdateUtil,
ytdlpViewModel: YTDLPViewModel,
preferences: SharedPreferences,
ytdlVersionPreference: Preference,
channel: String? = null
) = host.hostLifecycleOwner.lifecycleScope.launch {
val view = host.hostView!!
Snackbar.make(view, context.getString(R.string.ytdl_updating_started),
Snackbar.LENGTH_LONG).show()
runCatching {
val res = updateUtil.updateYTDL(channel)
when (res.status) {
UpdateUtil.YTDLPUpdateStatus.DONE -> {
Snackbar.make(view, res.message, Snackbar.LENGTH_LONG).show()
setYTDLPVersion(context, host, ytdlpViewModel, preferences, ytdlVersionPreference)
val infoJsonPath = FileUtil.getInfoJsonPath(context)
File(infoJsonPath).deleteRecursively()
}
UpdateUtil.YTDLPUpdateStatus.ALREADY_UP_TO_DATE -> Snackbar.make(view,
context.getString(R.string.you_are_in_latest_version),
Snackbar.LENGTH_LONG).show()
UpdateUtil.YTDLPUpdateStatus.ERROR -> {
val msg = res.message
view.apply {
val snackBar = Snackbar.make(this, msg, Snackbar.LENGTH_LONG)
snackBar.setAction(R.string.copy_log){
UiUtil.copyToClipboard(msg, host.getHostContext())
}
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()
}
}
else -> {
}
}
}.onFailure {
val msg = it.message ?: context.getString(R.string.errored)
view.apply {
val snackBar = Snackbar.make(this, msg, Snackbar.LENGTH_LONG)
snackBar.setAction(R.string.copy_log){
UiUtil.copyToClipboard(msg, host.getHostContext())
}
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()
}
}
}
}

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.deniscerri.ytdl.ui.more.settings.SettingsActivity">
tools:context=".ui.more.settings.SettingsActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/frame_layout"
@ -23,6 +24,7 @@
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -42,11 +44,39 @@
app:navigationIcon="@drawable/ic_back"
app:navigationContentDescription="@string/back"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"/>
android:layout_height="?attr/actionBarSize"
app:menu="@menu/settings_menu" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways|snap">
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:hint="@string/search" />
</FrameLayout>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.search.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/search"
app:layout_anchor="@id/search_bar">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_suggestions_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.search.SearchView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:paddingEnd="16dp"
android:paddingStart="0dp"
android:paddingVertical="16dp"
android:minHeight="48dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/preference_icon"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:outlineProvider="none"
android:stateListAnimator="@null"
android:clickable="false"
android:focusable="false"
app:iconTint="?android:colorAccent"
android:visibility="gone"
app:iconSize="25dp"
app:icon="@drawable/ic_down" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/preference_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="2" />
<TextView
android:id="@+id/preference_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:ellipsize="end"
android:maxLines="10"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:paddingEnd="16dp"
android:paddingStart="0dp"
android:paddingVertical="16dp"
android:minHeight="48dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/preference_icon"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:outlineProvider="none"
android:stateListAnimator="@null"
android:clickable="false"
android:focusable="false"
app:iconTint="?android:colorAccent"
android:visibility="gone"
app:iconSize="25dp"
app:icon="@drawable/ic_down" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/preference_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="2" />
<TextView
android:id="@+id/preference_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:ellipsize="end"
android:maxLines="10"
android:visibility="gone" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="wrap_content">
<SeekBar
android:id="@+id/seekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:padding="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/seekbarValue"
android:max="100" />
<TextView
android:id="@+id/seekbarValue"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</LinearLayout>

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:paddingEnd="16dp"
android:paddingStart="0dp"
android:paddingVertical="16dp"
android:minHeight="48dp"
android:gravity="center_vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/preference_icon"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:outlineProvider="none"
android:stateListAnimator="@null"
android:clickable="false"
android:focusable="false"
android:visibility="gone"
app:iconTint="?android:colorAccent"
app:iconSize="25dp"
app:icon="@drawable/ic_down" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/preference_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:ellipsize="end"
android:textStyle="bold"
android:maxLines="2" />
<TextView
android:id="@+id/preference_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:ellipsize="end"
android:maxLines="10"
android:visibility="gone" />
</LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/preference_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:focusable="false"
android:clickable="false" />
</LinearLayout>

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:paddingTop="16dp"
android:paddingHorizontal="50dp">
<TextView
android:id="@+id/preference_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/colorPrimary"
android:textStyle="bold"
android:textSize="13sp"
tools:text="Title"
android:ellipsize="end"
android:maxLines="2" />
</LinearLayout>

@ -0,0 +1,13 @@
<?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:id="@+id/search"
android:visible="false"
android:title="@string/search"
android:icon="@drawable/ic_search"
app:showAsAction="always" />
</menu>

@ -125,15 +125,15 @@
<navigation android:id="@+id/settingsNav">
<fragment
android:id="@+id/folderSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.FolderSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.folder.FolderSettingsFragment"
android:label="FolderSettingsFragment" />
<fragment
android:id="@+id/processingSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.ProcessingSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.processing.ProcessingSettingsFragment"
android:label="ProcessingSettingsFragment" />
<fragment
android:id="@+id/downloadSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.DownloadSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.downloading.DownloadSettingsFragment"
android:label="DownloadSettingsFragment" />
<fragment
android:id="@+id/updateSettingsFragment"
@ -161,7 +161,7 @@
</fragment>
<fragment
android:id="@+id/appearanceSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.GeneralSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.general.GeneralSettingsFragment"
android:label="AppearanceSettingsFragment" />
</navigation>
<dialog

@ -4,15 +4,15 @@
app:startDestination="@id/mainSettingsFragment">
<fragment
android:id="@+id/appearanceSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.GeneralSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.general.GeneralSettingsFragment"
android:label="AppearanceSettingsFragment" />
<fragment
android:id="@+id/processingSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.ProcessingSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.processing.ProcessingSettingsFragment"
android:label="ProcessingSettingsFragment" />
<fragment
android:id="@+id/folderSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.FolderSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.folder.FolderSettingsFragment"
android:label="FolderSettingsFragment" />
<fragment
android:id="@+id/updateSettingsFragment"
@ -27,7 +27,7 @@
</fragment>
<fragment
android:id="@+id/downloadSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.DownloadSettingsFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.downloading.DownloadSettingsFragment"
android:label="DownloadSettingsFragment" />
<fragment
android:id="@+id/mainSettingsFragment"

Loading…
Cancel
Save