diff --git a/app/src/main/java/com/deniscerri/ytdl/database/models/SearchSettingsItem.kt b/app/src/main/java/com/deniscerri/ytdl/database/models/SearchSettingsItem.kt new file mode 100644 index 00000000..dfd641c2 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/database/models/SearchSettingsItem.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/IconsSheetAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/IconsSheetAdapter.kt index e1b18536..f72f73a3 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/IconsSheetAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/IconsSheetAdapter.kt @@ -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() { +class IconsSheetAdapter(val host: SettingHost) : RecyclerView.Adapter() { class IconsSheetViewHolder( val binding: AppIconItemBinding @@ -29,11 +30,12 @@ class IconsSheetAdapter(val activity: Activity) : RecyclerView.Adapter(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(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 + 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) } - } - .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) + }, 300) + }, 200) } - /** - * Otherwise show the normal dialog, dialogs for other preference types are not supported yet - */ - else -> super.onDisplayPreferenceDialog(preference) } } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DefaultPreferenceActions.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DefaultPreferenceActions.kt new file mode 100644 index 00000000..ef9586b3 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DefaultPreferenceActions.kt @@ -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(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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DownloadSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DownloadSettingsFragment.kt deleted file mode 100644 index 7adccfab..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DownloadSettingsFragment.kt +++ /dev/null @@ -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("remember_download_type") - val downloadType = findPreference("preferred_download_type") - downloadType?.isEnabled = rememberDownloadType?.isChecked == false - rememberDownloadType?.setOnPreferenceClickListener { - downloadType?.isEnabled = !rememberDownloadType.isChecked - true - } - - val preventDuplicateDownloads = findPreference("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("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() - .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("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("use_scheduler") - val scheduleStart = findPreference("schedule_start") - scheduleStart?.summary = preferences.getString("schedule_start", "00:00") - val scheduleEnd = findPreference("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() - .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("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("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("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("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("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("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()) - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt deleted file mode 100644 index 02dbb1f7..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt +++ /dev/null @@ -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() - .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("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 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt deleted file mode 100644 index ab4f3573..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt +++ /dev/null @@ -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("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("label_visibility")?.apply { - isVisible = !resources.getBoolean(R.bool.uses_side_nav) - setOnPreferenceChangeListener { _, _ -> - ThemeUtil.recreateMain() - true - } - } - - findPreference("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(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("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("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(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("theme_accent")?.apply { - summary = entry - setOnPreferenceChangeListener { _, _ -> - ThemeUtil.updateThemes() - true - } - } - - findPreference("high_contrast")?.apply { - setOnPreferenceChangeListener { _, _ -> - ThemeUtil.updateThemes() - true - } - } - - findPreference("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("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("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("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("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("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("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("api_key")?.isVisible = newValue == "yt_api" - findPreference("custom_home_recommendation_url")?.isVisible = newValue == "custom" - - lifecycleScope.launch { - withContext(Dispatchers.IO){ - resultViewModel.deleteAll() - } - } - - true - } - } - - findPreference("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("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("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("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("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("ignore_battery")?.isVisible = false - } - super.onResume() - } - - private var displayOverAppsResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { _ -> - findNavController().popBackStack(R.id.appearanceSettingsFragment, false) - } - - -} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/PreferenceActivityResultDelegate.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/PreferenceActivityResultDelegate.kt new file mode 100644 index 00000000..72745815 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/PreferenceActivityResultDelegate.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/ProcessingSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/ProcessingSettingsFragment.kt deleted file mode 100644 index 89993113..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/ProcessingSettingsFragment.kt +++ /dev/null @@ -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("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("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("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("audio_codec") - val videoCodecPref = findPreference("video_codec") - val videoContainerPref = findPreference("video_format") - - val recodeVideoPreference = findPreference("recode_video")!! - val compatibleVideoPreference = findPreference("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("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 - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingHost.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingHost.kt new file mode 100644 index 00000000..80e7dfa5 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingHost.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingModule.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingModule.kt new file mode 100644 index 00000000..60d9cd6b --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingModule.kt @@ -0,0 +1,7 @@ +package com.deniscerri.ytdl.ui.more.settings + +import androidx.preference.Preference + +interface SettingModule { + fun bindLogic(pref: Preference, host: SettingHost) +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingsActivity.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingsActivity.kt index 8ed3788b..a3903434 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingsActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingsActivity.kt @@ -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() + + 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(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 } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingsRegistry.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingsRegistry.kt new file mode 100644 index 00000000..ea60c50b --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingsRegistry.kt @@ -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() + fragment.getPreferences(fragment.preferenceScreen, allPrefs).forEach { + module.bindLogic(it, fragment) + } + } + + fun indexAll(context: Context): List { + val manager = PreferenceManager(context) + val results = mutableListOf() + + 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 { + val list = mutableListOf() + + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsFragment.kt index bf4b4dec..b0a92a03 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsFragment.kt @@ -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("yt_player_client")?.setOnPreferenceClickListener { - findNavController().navigate(R.id.youtubePlayerClientFragment) - false - } - - findPreference("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("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>, onChange: (items: List>) -> 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() - } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsModule.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsModule.kt new file mode 100644 index 00000000..e1f56e19 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/AdvancedSettingsModule.kt @@ -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>, onChange: (items: List>) -> 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/downloading/DownloadSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/downloading/DownloadSettingsFragment.kt new file mode 100644 index 00000000..1bbb8fbe --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/downloading/DownloadSettingsFragment.kt @@ -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("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 + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/downloading/DownloadSettingsModule.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/downloading/DownloadSettingsModule.kt new file mode 100644 index 00000000..939edf79 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/downloading/DownloadSettingsModule.kt @@ -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() + .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() + .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 + } + } + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/folder/FolderSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/folder/FolderSettingsFragment.kt new file mode 100644 index 00000000..6bed0029 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/folder/FolderSettingsFragment.kt @@ -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("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("access_all_files")!!.isVisible = false + findPreference("cache_downloads")!!.isEnabled = true + }else{ + editor.putBoolean("cache_downloads", true).apply() + findPreference("cache_downloads")!!.isEnabled = false + } + super.onResume() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/folder/FolderSettingsModule.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/folder/FolderSettingsModule.kt new file mode 100644 index 00000000..fbfd948e --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/folder/FolderSettingsModule.kt @@ -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() + .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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/general/GeneralSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/general/GeneralSettingsFragment.kt new file mode 100644 index 00000000..42a78641 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/general/GeneralSettingsFragment.kt @@ -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("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("ignore_battery")?.isVisible = false + } + super.onResume() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/general/GeneralSettingsModule.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/general/GeneralSettingsModule.kt new file mode 100644 index 00000000..e87b45a5 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/general/GeneralSettingsModule.kt @@ -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(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(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 + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/processing/ProcessingSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/processing/ProcessingSettingsFragment.kt new file mode 100644 index 00000000..4e27699e --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/processing/ProcessingSettingsFragment.kt @@ -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("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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/processing/ProcessingSettingsModule.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/processing/ProcessingSettingsModule.kt new file mode 100644 index 00000000..95379c95 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/processing/ProcessingSettingsModule.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/search/SettingsSearchAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/search/SettingsSearchAdapter.kt new file mode 100644 index 00000000..3838dda9 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/search/SettingsSearchAdapter.kt @@ -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, + private val activity: SettingsActivity +) : RecyclerView.Adapter() { + + @SuppressLint("NotifyDataSetChanged") + fun updateList(newList: List) { + 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 + ) { + 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsFragment.kt index 72290a78..23a2ab9a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsFragment.kt @@ -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("changelog")?.setOnPreferenceClickListener { - findNavController().navigate(R.id.changeLogFragment) - false - } - - findPreference("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("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(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(com.google.android.material.R.id.snackbar_text) as TextView - snackTextView.maxLines = 9999999 - snackBar.show() - } - - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsModule.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsModule.kt new file mode 100644 index 00000000..5dbec3b0 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsModule.kt @@ -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(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(com.google.android.material.R.id.snackbar_text) as TextView + snackTextView.maxLines = 9999999 + snackBar.show() + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 089adf1d..0b68368c 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -1,11 +1,12 @@ - + tools:context=".ui.more.settings.SettingsActivity"> @@ -34,19 +36,47 @@ app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" android:layout_height="?attr/collapsingToolbarLayoutLargeSize"> - + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + app:menu="@menu/settings_menu" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_search_result_regular.xml b/app/src/main/res/layout/preference_search_result_regular.xml new file mode 100644 index 00000000..84e7202a --- /dev/null +++ b/app/src/main/res/layout/preference_search_result_regular.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_search_result_seekbar.xml b/app/src/main/res/layout/preference_search_result_seekbar.xml new file mode 100644 index 00000000..cbc8c92a --- /dev/null +++ b/app/src/main/res/layout/preference_search_result_seekbar.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_search_result_switch.xml b/app/src/main/res/layout/preference_search_result_switch.xml new file mode 100644 index 00000000..1359d79d --- /dev/null +++ b/app/src/main/res/layout/preference_search_result_switch.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_search_result_title.xml b/app/src/main/res/layout/preference_search_result_title.xml new file mode 100644 index 00000000..afa7df65 --- /dev/null +++ b/app/src/main/res/layout/preference_search_result_title.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/settings_menu.xml b/app/src/main/res/menu/settings_menu.xml new file mode 100644 index 00000000..50fc9478 --- /dev/null +++ b/app/src/main/res/menu/settings_menu.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index c6c04cde..bc821d01 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -125,15 +125,15 @@