more setting fixes

pull/1129/head
deniscerri 7 months ago
parent 46bc8e7c13
commit 9aa81b6001
No known key found for this signature in database
GPG Key ID: 95C43D517D830350

@ -5,22 +5,37 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
import androidx.work.WorkManager
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.models.LogItem
import com.deniscerri.ytdl.database.models.RestoreAppDataItem
import com.deniscerri.ytdl.database.models.SearchSettingsItem
import com.deniscerri.ytdl.database.repository.CommandTemplateRepository
import com.deniscerri.ytdl.database.repository.CookieRepository
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdl.database.repository.SearchHistoryRepository
import com.deniscerri.ytdl.ui.more.settings.SettingsRegistry
import com.deniscerri.ytdl.util.BackupSettingsUtil
import com.deniscerri.ytdl.util.Extensions.combine
import com.deniscerri.ytdl.util.FileUtil
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Calendar
@ -37,6 +52,10 @@ class SettingsViewModel(private val application: Application) : AndroidViewModel
private val searchHistoryRepository : SearchHistoryRepository
private val observeSourcesRepository : ObserveSourcesRepository
private val _settingsFlow = MutableStateFlow<List<SearchSettingsItem>>(emptyList())
val settingsFlow: StateFlow<List<SearchSettingsItem>>
private val _searchQuery = MutableStateFlow("")
init {
val dbManager = DBManager.getInstance(application)
historyRepository = HistoryRepository(dbManager.historyDao)
@ -45,6 +64,30 @@ class SettingsViewModel(private val application: Application) : AndroidViewModel
commandTemplateRepository = CommandTemplateRepository(dbManager.commandTemplateDao)
searchHistoryRepository = SearchHistoryRepository(dbManager.searchHistoryDao)
observeSourcesRepository = ObserveSourcesRepository(dbManager.observeSourcesDao, workManager, preferences)
settingsFlow = combine(_settingsFlow, _searchQuery) { items, query ->
if (query.isBlank()) {
emptyList() // Or return items if you want to show everything when blank
} else {
items.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
(titleMatch || summaryMatch || groupMatch)
}
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
fun indexSearchSettings() {
viewModelScope.launch(Dispatchers.IO) {
val indexedItems = SettingsRegistry.indexAll(App.instance)
_settingsFlow.emit(indexedItems)
}
}
fun setSearchQuery(query: String) {
_searchQuery.value = query
}
suspend fun backup(items: List<String> = listOf()) : Result<String> {

@ -7,7 +7,10 @@ import android.view.View
import androidx.activity.addCallback
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
@ -15,10 +18,12 @@ 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.database.viewmodel.SettingsViewModel
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.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.lazy
@ -26,20 +31,16 @@ import kotlin.lazy
class SettingsActivity : BaseActivity(), SettingHost {
var context: Context? = null
private lateinit var settingViewModel: SettingsViewModel
private lateinit var navController: NavController
private lateinit var searchAdapter: SettingsSearchAdapter
private var allIndexedItems = listOf<SearchSettingsItem>()
override fun findPref(key: String): Preference? {
return allIndexedItems.find { it.preference.key == key }?.preference
return settingViewModel.settingsFlow.value.find { it.preference.key == key }?.preference
}
@SuppressLint("NotifyDataSetChanged")
override fun refreshUI() {
binding.searchSuggestionsRecycler.post {
if (!isFinishing && !isDestroyed) {
searchAdapter.notifyDataSetChanged()
}
}
settingViewModel.indexSearchSettings()
}
override fun getHostContext() = this
override val activityResultDelegate = PreferenceActivityResultDelegate(this)
@ -74,17 +75,20 @@ class SettingsActivity : BaseActivity(), SettingHost {
super.onCreate(savedInstanceState)
context = baseContext
binding = ActivitySettingsBinding.inflate(layoutInflater)
settingViewModel = ViewModelProvider(this)[SettingsViewModel::class.java]
setContentView(binding.root)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.frame_layout) as NavHostFragment
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.frame_layout) as NavHostFragment
navController = navHostFragment.findNavController()
val listener = NavController.OnDestinationChangedListener { controller, destination, arguments ->
if (destination.id == R.id.mainSettingsFragment){
changeTopAppbarTitle(getString(R.string.settings), false)
indexSettings()
val listener =
NavController.OnDestinationChangedListener { controller, destination, arguments ->
if (destination.id == R.id.mainSettingsFragment) {
changeTopAppbarTitle(getString(R.string.settings), false)
settingViewModel.indexSearchSettings()
}
}
}
navController.addOnDestinationChangedListener(listener)
binding.settingsToolbar.setNavigationOnClickListener {
@ -97,7 +101,7 @@ class SettingsActivity : BaseActivity(), SettingHost {
} else if (navController.currentDestination?.id == R.id.mainSettingsFragment) {
navController.popBackStack()
finishAndRemoveTask()
}else{
} else {
navController.navigateUp()
}
}
@ -105,56 +109,11 @@ class SettingsActivity : BaseActivity(), SettingHost {
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
if (totalScrollRange == 0) return@addOnOffsetChangedListener
val percentage = Math.abs(verticalOffset).toFloat() / totalScrollRange
val isCollapsed = percentage > 0.8f
// 1. Handle Menu Icon Visibility
toolbar.menu.findItem(R.id.search)?.isVisible = isCollapsed
// 2. Handle SearchBar
when {
percentage >= 1f -> {
// Fully collapsed — safe to GONE now, jump isn't visible
if (searchBar.visibility != View.GONE) searchBar.visibility = View.GONE
}
percentage > 0.8f -> {
// Mid-collapse — hide visually but keep space to avoid jump
searchBar.visibility = View.INVISIBLE
searchBar.alpha = 0f
}
else -> {
// Expanding — restore and fade in/out
if (searchBar.visibility != View.VISIBLE) searchBar.visibility = View.VISIBLE
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)
}
}
settingViewModel.indexSearchSettings()
searchAdapter = SettingsSearchAdapter(emptyList(), this)
binding.searchSuggestionsRecycler.layoutManager = LinearLayoutManager(context)
@ -162,45 +121,34 @@ class SettingsActivity : BaseActivity(), SettingHost {
binding.searchSuggestionsRecycler.itemAnimator = null
binding.searchView.editText.addTextChangedListener { text ->
filterSettings(text.toString())
settingViewModel.setSearchQuery(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)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
settingViewModel.settingsFlow.collect { allItems ->
allItems.forEach { item ->
if (!item.isHeader) {
item.module?.bindLogic(item.preference, this@SettingsActivity)
}
}
val visibleItems = allItems.filter { it.preference.isVisible }
searchAdapter.updateList(visibleItems)
val savedSearch = intent.getStringExtra("search_query")
if (!savedSearch.isNullOrBlank()) {
binding.searchBar.performClick()
intent.removeExtra("search_query")
settingViewModel.setSearchQuery(savedSearch)
}
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)
override fun onResume() {
refreshUI()
super.onResume()
}
fun closeSearchView() {
@ -210,6 +158,7 @@ class SettingsActivity : BaseActivity(), SettingHost {
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
binding.settingsToolbar.menu.findItem(R.id.search)?.isVisible =
!binding.searchBar.isVisible && !hideSearch
}
}

@ -71,6 +71,7 @@ object DownloadSettingsModule : SettingModule {
preferences.edit(commit = true) {
putString("download_archive_path", path)
}
pref.summary = FileUtil.getDownloadArchivePath(context)
host.refreshUI()
}
true

@ -109,7 +109,7 @@ object FolderSettingsModule: SettingModule {
}
}
pref.apply {
summary = FileUtil.formatPath(preferences.getString("command_path", "")!!)
summary = FileUtil.formatPath(preferences.getString(pref.key, "")!!)
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
@ -191,14 +191,15 @@ object FolderSettingsModule: SettingModule {
putBoolean("keep_cache", false)
}
}
host.refreshUI()
true
}
}
}
"keep_cache" -> {
(pref as SwitchPreferenceCompat).apply {
val noFragments = host.findPref("no_part") as SwitchPreferenceCompat
if (noFragments.isChecked) {
val noFragments = preferences.getBoolean("no_part", false)
if (noFragments) {
isEnabled = false
isChecked = false
} else {
@ -229,6 +230,7 @@ object FolderSettingsModule: SettingModule {
preferences.edit(commit = true) {
putString(pref.key, it)
}
summary = it
host.refreshUI()
}
false
@ -246,6 +248,7 @@ object FolderSettingsModule: SettingModule {
preferences.edit(commit = true) {
putString(pref.key, it)
}
summary = it
host.refreshUI()
}
false
@ -287,6 +290,16 @@ object FolderSettingsModule: SettingModule {
}else{
Snackbar.make(host.hostView!!, context.getString(R.string.downloads_running_try_later), Snackbar.LENGTH_SHORT).show()
}
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}) "
host.refreshUI()
}
true

@ -194,7 +194,7 @@ object GeneralSettingsModule : SettingModule {
}
}
preferences.edit(commit = true){
putString("ytdlnis_theme", newValue.toString())
putString(pref.key, newValue.toString())
}
ThemeUtil.updateThemes()
host.refreshUI()
@ -362,9 +362,14 @@ object GeneralSettingsModule : SettingModule {
withContext(Dispatchers.IO) {
resultViewModel.deleteAll()
}
val s = context.getString(R.string.video_recommendations_summary)
summary = if ((newValue as String).isBlank()) {
s
}else {
"${s}\n[${entries[entryValues.indexOf(value)]}]"
}
host.refreshUI()
}
host.refreshUI()
true
}
}
@ -400,6 +405,12 @@ object GeneralSettingsModule : SettingModule {
resultViewModel.deleteAll()
}
}
val s = context.getString(R.string.api_key_summary)
summary = if ((newValue as String).isBlank()) {
s
}else {
"${s}\n[${text}]"
}
host.refreshUI()
true
}
@ -414,6 +425,11 @@ object GeneralSettingsModule : SettingModule {
"${s}\n[${entries[entryValues.indexOf(value)]}]"
}
setOnPreferenceChangeListener { _, newValue ->
summary = if (value.isNullOrBlank()) {
s
}else {
"${s}\n[${entries[entryValues.indexOf(value)]}]"
}
host.refreshUI()
true
}

@ -1,6 +1,9 @@
package com.deniscerri.ytdl.ui.more.settings.processing
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
@ -11,6 +14,7 @@ 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 kotlinx.coroutines.launch
import kotlin.collections.indexOf
object ProcessingSettingsModule : SettingModule {
@ -111,83 +115,97 @@ object ProcessingSettingsModule : SettingModule {
}
}
"audio_codec" -> {
updateCompatibleVideoConfig(host)
updateCompatibleVideoConfig(host, prefs)
}
"video_codec" -> {
updateCompatibleVideoConfig(host)
updateCompatibleVideoConfig(host, prefs)
}
"video_format" -> {
updateCompatibleVideoConfig(host)
updateCompatibleVideoConfig(host, prefs)
}
"recode_video" -> {
val compatibleVideoPreference = host.findPref("compatible_video") as SwitchPreferenceCompat
pref.setOnPreferenceClickListener {
if (compatibleVideoPreference.isChecked && (pref as SwitchPreferenceCompat).isChecked) {
compatibleVideoPreference.performClick()
}
pref.setOnPreferenceChangeListener { _, newValue ->
recodeVideoClicked(context, host, prefs, newValue as Boolean)
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()
}
pref.setOnPreferenceChangeListener { _, newValue ->
compatibleVideoClicked(context, host, prefs, newValue as Boolean)
true
}
}
}
}
fun updateCompatibleVideoConfig(host: SettingHost) {
fun updateCompatibleVideoConfig(host: SettingHost, prefs: SharedPreferences) {
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
val compatibleVideoPreference = prefs.getBoolean("compatible_video", false)
audioCodecPref?.isEnabled = !compatibleVideoPreference
videoCodecPref?.isEnabled = !compatibleVideoPreference
videoContainerPref?.isEnabled = !compatibleVideoPreference
}
audioCodecPref?.isEnabled = !compatibleVideoPreference.isChecked
videoCodecPref?.isEnabled = !compatibleVideoPreference.isChecked
videoContainerPref?.isEnabled = !compatibleVideoPreference.isChecked
fun recodeVideoClicked(context: Context, host: SettingHost, prefs: SharedPreferences, isChecked: Boolean) {
val compatibleVideoPreference = prefs.getBoolean("compatible_video", false)
if (compatibleVideoPreference && isChecked) {
prefs.edit(commit = true) {
putBoolean("recode_video", isChecked)
putBoolean("compatible_video", false)
}
compatibleVideoClicked(context, host, prefs, !compatibleVideoPreference)
} else {
host.refreshUI()
}
}
fun compatibleVideoClicked(context: Context, host: SettingHost, prefs: SharedPreferences, isChecked: Boolean) {
if (isChecked) {
val recodeVideoPreference = prefs.getBoolean("recode_video", false)
if (recodeVideoPreference) {
prefs.edit(commit = true) {
putBoolean("compatible_video", isChecked)
putBoolean("recode_video", false)
}
recodeVideoClicked(context, host, prefs, !recodeVideoPreference)
}
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()
}
}
}

@ -93,7 +93,7 @@ class SettingsSearchAdapter(
private fun bindVisualsOnly(holder: RecyclerView.ViewHolder, position: Int) {
val item = items[position]
val pref = item.preference
if (pref.title.isNullOrBlank())
if (pref.title.isNullOrBlank()) return
if (!item.isHeader && item.canRebind) {
item.module?.bindLogic(pref, activity)

@ -61,8 +61,7 @@
app:navigationIcon="@drawable/ic_back"
app:navigationContentDescription="@string/back"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:menu="@menu/settings_menu" />
android:layout_height="?attr/actionBarSize"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>

Loading…
Cancel
Save