settings: redocument

Redocument the settings module.
This commit is contained in:
Alexander Capehart 2022-12-24 16:12:32 -07:00
parent 18ba845302
commit b086c44b59
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
18 changed files with 253 additions and 176 deletions

View file

@ -75,7 +75,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
// Save any pending tab configurations to restore from when this dialog is re-created. // Save any pending tab configurations to restore if this dialog is re-created.
outState.putInt(KEY_TABS, Tab.toIntCode(tabAdapter.tabs)) outState.putInt(KEY_TABS, Tab.toIntCode(tabAdapter.tabs))
} }

View file

@ -44,16 +44,16 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
override fun getItemCount() = currentList.size override fun getItemCount() = currentList.size
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) { override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
if (payloads.isEmpty()) {
// Not updating any indicator-specific things, so delegate to the concrete
// adapter (actually bind the item)
onBindViewHolder(holder, position)
}
// Only try to update the playing indicator if the ViewHolder supports it // Only try to update the playing indicator if the ViewHolder supports it
if (holder is ViewHolder) { if (holder is ViewHolder) {
holder.updatePlayingIndicator(currentList[position] == currentItem, isPlaying) holder.updatePlayingIndicator(currentList[position] == currentItem, isPlaying)
} }
if (payloads.isEmpty()) {
// Not updating any indicator-specific attributes, so delegate to the concrete
// adapter (actually bind the item)
onBindViewHolder(holder, position)
}
} }
/** /**
* Update the currently playing item in the list. * Update the currently playing item in the list.

View file

@ -15,6 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// We use a special naming convention for internal fields, disable the lints that check for that.
@file:Suppress("PropertyName", "FunctionName") @file:Suppress("PropertyName", "FunctionName")
package org.oxycblt.auxio.music package org.oxycblt.auxio.music

View file

@ -139,7 +139,7 @@ fun List<String>.parseMultiValue(settings: Settings) =
*/ */
fun String.maybeParseSeparators(settings: Settings): List<String> { fun String.maybeParseSeparators(settings: Settings): List<String> {
// Get the separators the user desires. If null, there's nothing to do. // Get the separators the user desires. If null, there's nothing to do.
val separators = settings.separators ?: return listOf(this) val separators = settings.musicSeparators ?: return listOf(this)
return splitEscaped { separators.contains(it) } return splitEscaped { separators.contains(it) }
} }

View file

@ -55,7 +55,7 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
if (binding.separatorSlash.isChecked) separators += SEPARATOR_SLASH if (binding.separatorSlash.isChecked) separators += SEPARATOR_SLASH
if (binding.separatorPlus.isChecked) separators += SEPARATOR_PLUS if (binding.separatorPlus.isChecked) separators += SEPARATOR_PLUS
if (binding.separatorAnd.isChecked) separators += SEPARATOR_AND if (binding.separatorAnd.isChecked) separators += SEPARATOR_AND
settings.separators = separators settings.musicSeparators = separators
} }
} }
@ -71,7 +71,7 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
// More efficient to do one iteration through the separator list and initialize // More efficient to do one iteration through the separator list and initialize
// the corresponding CheckBox for each character instead of doing an iteration // the corresponding CheckBox for each character instead of doing an iteration
// through the separator list for each CheckBox. // through the separator list for each CheckBox.
settings.separators?.forEach { settings.musicSeparators?.forEach {
when (it) { when (it) {
SEPARATOR_COMMA -> binding.separatorComma.isChecked = true SEPARATOR_COMMA -> binding.separatorComma.isChecked = true
SEPARATOR_SEMICOLON -> binding.separatorSemicolon.isChecked = true SEPARATOR_SEMICOLON -> binding.separatorSemicolon.isChecked = true

View file

@ -85,7 +85,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
} }
private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, settings: Settings) { private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, settings: Settings) {
when (settings.actionMode) { when (settings.playbackBarAction) {
ActionMode.NEXT -> { ActionMode.NEXT -> {
binding.playbackSecondaryAction.apply { binding.playbackSecondaryAction.apply {
setIconResource(R.drawable.ic_skip_next_24) setIconResource(R.drawable.ic_skip_next_24)

View file

@ -342,7 +342,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
// Android 13+ leverages custom actions in the notification. // Android 13+ leverages custom actions in the notification.
val extraAction = val extraAction =
when (settings.notifAction) { when (settings.playbackNotificationAction) {
ActionMode.SHUFFLE -> ActionMode.SHUFFLE ->
PlaybackStateCompat.CustomAction.Builder( PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INVERT_SHUFFLE, PlaybackService.ACTION_INVERT_SHUFFLE,
@ -375,7 +375,7 @@ class MediaSessionComponent(private val context: Context, private val callback:
private fun invalidateSecondaryAction() { private fun invalidateSecondaryAction() {
invalidateSessionState() invalidateSessionState()
when (settings.notifAction) { when (settings.playbackNotificationAction) {
ActionMode.SHUFFLE -> notification.updateShuffled(playbackManager.isShuffled) ActionMode.SHUFFLE -> notification.updateShuffled(playbackManager.isShuffled)
else -> notification.updateRepeatMode(playbackManager.repeatMode) else -> notification.updateRepeatMode(playbackManager.repeatMode)
} }

View file

@ -27,7 +27,6 @@ import androidx.core.net.toUri
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.transition.MaterialFadeThrough import com.google.android.material.transition.MaterialFadeThrough
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
@ -41,7 +40,7 @@ import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
/** /**
* A [BottomSheetDialogFragment] that shows Auxio's about screen. * A [ViewBindingFragment] that displays information about the app and the current music library.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() { class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
@ -56,18 +55,21 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
override fun onCreateBinding(inflater: LayoutInflater) = FragmentAboutBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = FragmentAboutBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentAboutBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentAboutBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
// --- UI SETUP ---
binding.aboutToolbar.setNavigationOnClickListener { findNavController().navigateUp() }
binding.aboutContents.setOnApplyWindowInsetsListener { view, insets -> binding.aboutContents.setOnApplyWindowInsetsListener { view, insets ->
view.updatePadding(bottom = insets.systemBarInsetsCompat.bottom) view.updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
insets insets
} }
binding.aboutToolbar.setNavigationOnClickListener { findNavController().navigateUp() }
binding.aboutVersion.text = BuildConfig.VERSION_NAME binding.aboutVersion.text = BuildConfig.VERSION_NAME
binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_CODEBASE) } binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_SOURCE) }
binding.aboutFaq.setOnClickListener { openLinkInBrowser(LINK_FAQ) } binding.aboutFaq.setOnClickListener { openLinkInBrowser(LINK_FAQ) }
binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) } binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) }
// VIEWMODEL SETUP
collectImmediately(musicModel.statistics, ::updateStatistics) collectImmediately(musicModel.statistics, ::updateStatistics)
} }
@ -86,14 +88,15 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
(statistics?.durationMs ?: 0).formatDurationMs(false)) (statistics?.durationMs ?: 0).formatDurationMs(false))
} }
/** Go through the process of opening a [link] in a browser. */ /**
private fun openLinkInBrowser(link: String) { * Open the given URI in a web browser.
logD("Opening $link") * @param uri The URL to open.
*/
private fun openLinkInBrowser(uri: String) {
logD("Opening $uri")
val context = requireContext() val context = requireContext()
val browserIntent = val browserIntent =
Intent(Intent.ACTION_VIEW, link.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) Intent(Intent.ACTION_VIEW, uri.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11 seems to now handle the app chooser situations on its own now // Android 11 seems to now handle the app chooser situations on its own now
@ -124,7 +127,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
browserIntent.setPackage(pkgName) browserIntent.setPackage(pkgName)
startActivity(browserIntent) startActivity(browserIntent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
// Not browser but an app chooser due to OEM garbage // Not a browser but an app chooser
browserIntent.setPackage(null) browserIntent.setPackage(null)
openAppChooser(browserIntent) openAppChooser(browserIntent)
} }
@ -136,18 +139,24 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
} }
} }
/**
* Open an app chooser for a given [Intent].
* @param intent The [Intent] to show an app chooser for.
*/
private fun openAppChooser(intent: Intent) { private fun openAppChooser(intent: Intent) {
val chooserIntent = val chooserIntent =
Intent(Intent.ACTION_CHOOSER) Intent(Intent.ACTION_CHOOSER)
.putExtra(Intent.EXTRA_INTENT, intent) .putExtra(Intent.EXTRA_INTENT, intent)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(chooserIntent) startActivity(chooserIntent)
} }
companion object { companion object {
private const val LINK_CODEBASE = "https://github.com/oxygencobalt/Auxio" /** The URL to the source code. */
private const val LINK_FAQ = "$LINK_CODEBASE/blob/master/info/FAQ.md" private const val LINK_SOURCE = "https://github.com/oxygencobalt/Auxio"
private const val LINK_LICENSES = "$LINK_CODEBASE/blob/master/info/LICENSES.md" /** The URL to the FAQ document. */
private const val LINK_FAQ = "$LINK_SOURCE/blob/master/info/FAQ.md"
/** The URL to the licenses document. */
private const val LINK_LICENSES = "$LINK_SOURCE/blob/master/info/LICENSES.md"
} }
} }

View file

@ -40,12 +40,8 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* Auxio's settings. * A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings.
* * Object mutability
* This object wraps [SharedPreferences] in a type-safe manner, allowing access to all of the major
* settings that Auxio uses. Mutability is determined by use, as some values are written by
* PreferenceManager and others are written by Auxio's code.
*
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class Settings(private val context: Context, private val callback: Callback? = null) : class Settings(private val context: Context, private val callback: Callback? = null) :
@ -59,8 +55,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
/** /**
* Try to migrate shared preference keys to their new versions. Only intended for use by * Migrate any settings from an old version into their modern counterparts. This can cause
* AuxioApp. Compat code will persist for 6 months before being removed. * data loss depending on the feasibility of a migration.
*/ */
fun migrate() { fun migrate() {
if (inner.contains(OldKeys.KEY_ACCENT3)) { if (inner.contains(OldKeys.KEY_ACCENT3)) {
@ -117,7 +113,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
fun Int.migratePlaybackMode() = fun Int.migratePlaybackMode() =
when (this) { when (this) {
// Genre playback mode was retried in 3.0.0 // Genre playback mode was removed in 3.0.0
IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS
IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS
IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS
@ -156,6 +152,10 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/**
* Release this instance and any callbacks held by it. This is not needed if no [Callback]
* was originally attached.
*/
fun release() { fun release() {
inner.unregisterOnSharedPreferenceChangeListener(this) inner.unregisterOnSharedPreferenceChangeListener(this)
} }
@ -164,25 +164,27 @@ class Settings(private val context: Context, private val callback: Callback? = n
unlikelyToBeNull(callback).onSettingChanged(key) unlikelyToBeNull(callback).onSettingChanged(key)
} }
/** An interface for receiving some preference updates. */ /**
* TODO: Remove this
*/
interface Callback { interface Callback {
fun onSettingChanged(key: String) fun onSettingChanged(key: String)
} }
// --- VALUES --- // --- VALUES ---
/** The current theme */ /** The current theme. Represented by the [AppCompatDelegate] constants. */
val theme: Int val theme: Int
get() = get() =
inner.getInt( inner.getInt(
context.getString(R.string.set_key_theme), context.getString(R.string.set_key_theme),
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
/** Whether the dark theme should be black or not */ /** Whether to use a black background when a dark theme is currently used. */
val useBlackTheme: Boolean val useBlackTheme: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_black_theme), false) get() = inner.getBoolean(context.getString(R.string.set_key_black_theme), false)
/** The current accent. */ /** The current [Accent] (Color Scheme). */
var accent: Accent var accent: Accent
get() = get() =
Accent.from(inner.getInt(context.getString(R.string.set_key_accent), Accent.DEFAULT)) Accent.from(inner.getInt(context.getString(R.string.set_key_accent), Accent.DEFAULT))
@ -193,7 +195,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** The current library tabs preferred by the user. */ /** The tabs to show in the home UI. */
var libTabs: Array<Tab> var libTabs: Array<Tab>
get() = get() =
Tab.fromIntCode( Tab.fromIntCode(
@ -206,40 +208,40 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** Whether to hide collaborator artists or not. */ /** Whether to hide artists considered "collaborators" from the home UI. */
val shouldHideCollaborators: Boolean val shouldHideCollaborators: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_hide_collaborators), false) get() = inner.getBoolean(context.getString(R.string.set_key_hide_collaborators), false)
/** Whether to round additional UI elements (including album covers) */ /** Whether to round additional UI elements that require album covers to be rounded. */
val roundMode: Boolean val roundMode: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_round_mode), false) get() = inner.getBoolean(context.getString(R.string.set_key_round_mode), false)
/** Which action to display on the playback bar. */ /** The action to display on the playback bar. */
val actionMode: ActionMode val playbackBarAction: ActionMode
get() = get() =
ActionMode.fromIntCode( ActionMode.fromIntCode(
inner.getInt(context.getString(R.string.set_key_bar_action), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_bar_action), Int.MIN_VALUE))
?: ActionMode.NEXT ?: ActionMode.NEXT
/** The custom action to display in the notification. */ /** The action to display in the playback notification. */
val notifAction: ActionMode val playbackNotificationAction: ActionMode
get() = get() =
ActionMode.fromIntCode( ActionMode.fromIntCode(
inner.getInt(context.getString(R.string.set_key_notif_action), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_notif_action), Int.MIN_VALUE))
?: ActionMode.REPEAT ?: ActionMode.REPEAT
/** Whether to resume playback when a headset is connected (may not work well in all cases) */ /** Whether to start playback when a headset is plugged in. */
val headsetAutoplay: Boolean val headsetAutoplay: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_headset_autoplay), false) get() = inner.getBoolean(context.getString(R.string.set_key_headset_autoplay), false)
/** The current ReplayGain configuration */ /** The current ReplayGain configuration. */
val replayGainMode: ReplayGainMode val replayGainMode: ReplayGainMode
get() = get() =
ReplayGainMode.fromIntCode( ReplayGainMode.fromIntCode(
inner.getInt(context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE))
?: ReplayGainMode.DYNAMIC ?: ReplayGainMode.DYNAMIC
/** The current ReplayGain pre-amp configuration */ /** The current ReplayGain pre-amp configuration. */
var replayGainPreAmp: ReplayGainPreAmp var replayGainPreAmp: ReplayGainPreAmp
get() = get() =
ReplayGainPreAmp( ReplayGainPreAmp(
@ -253,7 +255,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** What queue to create when a song is selected from the library or search */ /** What MusicParent item to play from when a Song is played from the home view. */
val libPlaybackMode: MusicMode val libPlaybackMode: MusicMode
get() = get() =
MusicMode.fromIntCode( MusicMode.fromIntCode(
@ -262,8 +264,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
?: MusicMode.SONGS ?: MusicMode.SONGS
/** /**
* What queue t create when a song is selected from an album/artist/genre. Null means to default * What MusicParent item to play from when a Song is played from the detail view.
* to the currently shown item. * Will be null if configured to play from the currently shown item.
*/ */
val detailPlaybackMode: MusicMode? val detailPlaybackMode: MusicMode?
get() = get() =
@ -271,18 +273,15 @@ class Settings(private val context: Context, private val callback: Callback? = n
inner.getInt( inner.getInt(
context.getString(R.string.set_key_detail_song_playback_mode), Int.MIN_VALUE)) context.getString(R.string.set_key_detail_song_playback_mode), Int.MIN_VALUE))
/** Whether shuffle should stay on when a new song is selected. */ /** Whether to keep shuffle on when playing a new Song. */
val keepShuffle: Boolean val keepShuffle: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_keep_shuffle), true) get() = inner.getBoolean(context.getString(R.string.set_key_keep_shuffle), true)
/** Whether to rewind when the back button is pressed. */ /** Whether to rewind when the skip previous button is pressed before skipping back. */
val rewindWithPrev: Boolean val rewindWithPrev: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_rewind_prev), true) get() = inner.getBoolean(context.getString(R.string.set_key_rewind_prev), true)
/** /** Whether a song should pause after every repeat. */
* Whether [org.oxycblt.auxio.playback.state.RepeatMode.TRACK] should pause when the track
* repeats
*/
val pauseOnRepeat: Boolean val pauseOnRepeat: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false) get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false)
@ -290,28 +289,34 @@ class Settings(private val context: Context, private val callback: Callback? = n
val shouldBeObserving: Boolean val shouldBeObserving: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_observing), false) get() = inner.getBoolean(context.getString(R.string.set_key_observing), false)
/** The strategy used when loading images. */ /** The strategy used when loading album covers. */
val coverMode: CoverMode val coverMode: CoverMode
get() = get() =
CoverMode.fromIntCode( CoverMode.fromIntCode(
inner.getInt(context.getString(R.string.set_key_cover_mode), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
?: CoverMode.MEDIA_STORE ?: CoverMode.MEDIA_STORE
/** Whether to load all audio files, even ones not considered music. */ /** Whether to exclude non-music audio files from the music library. */
val excludeNonMusic: Boolean val excludeNonMusic: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_exclude_non_music), true) get() = inner.getBoolean(context.getString(R.string.set_key_exclude_non_music), true)
/** Get the list of directories that music should be hidden/loaded from. */ /**
* Set the configuration on how to handle particular directories in the music library.
* @param storageManager [StorageManager] required to parse directories.
* @return The [MusicDirectories] configuration.
*/
fun getMusicDirs(storageManager: StorageManager): MusicDirectories { fun getMusicDirs(storageManager: StorageManager): MusicDirectories {
val dirs = val dirs =
(inner.getStringSet(context.getString(R.string.set_key_music_dirs), null) ?: emptySet()) (inner.getStringSet(context.getString(R.string.set_key_music_dirs), null) ?: emptySet())
.mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) } .mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) }
return MusicDirectories( return MusicDirectories(
dirs, inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false)) dirs, inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false))
} }
/** Set the list of directories that music should be hidden/loaded from. */ /**
* Set the configuration on how to handle particular directories in the music library.
* @param musicDirs The new [MusicDirectories] configuration.
*/
fun setMusicDirs(musicDirs: MusicDirectories) { fun setMusicDirs(musicDirs: MusicDirectories) {
inner.edit { inner.edit {
putStringSet( putStringSet(
@ -323,8 +328,11 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** The list of separators the user wants to parse by. */ /**
var separators: String? * A string of characters representing the desired separator characters to denote
* multi-value tags.
*/
var musicSeparators: String?
// Differ from convention and store a string of separator characters instead of an int // Differ from convention and store a string of separator characters instead of an int
// code. This makes it easier to use in Regexes and makes it more extendable. // code. This makes it easier to use in Regexes and makes it more extendable.
get() = get() =
@ -336,7 +344,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** The current filter mode of the search tab */ /** The type of Music the search view is currently filtering to. */
var searchFilterMode: MusicMode? var searchFilterMode: MusicMode?
get() = get() =
MusicMode.fromIntCode( MusicMode.fromIntCode(
@ -350,7 +358,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** The song sort mode on HomeFragment */ /** The Song [Sort] mode used in the Home UI. */
var libSongSort: Sort var libSongSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
@ -363,7 +371,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** The album sort mode on HomeFragment */ /** The Album [Sort] mode used in the Home UI. */
var libAlbumSort: Sort var libAlbumSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
@ -376,7 +384,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** The artist sort mode on HomeFragment */ /** The Artist [Sort] mode used in the Home UI. */
var libArtistSort: Sort var libArtistSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
@ -389,7 +397,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** The genre sort mode on HomeFragment */ /** The Genre [Sort] mode used in the Home UI. */
var libGenreSort: Sort var libGenreSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
@ -402,7 +410,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** The detail album sort mode */ /** The [Sort] mode used in the Album Detail UI. */
var detailAlbumSort: Sort var detailAlbumSort: Sort
get() { get() {
var sort = var sort =
@ -425,7 +433,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** The detail artist sort mode */ /** The [Sort] mode used in the Artist Detail UI. */
var detailArtistSort: Sort var detailArtistSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
@ -438,7 +446,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** The detail genre sort mode */ /** The [Sort] mode used in the Genre Detail UI. */
var detailGenreSort: Sort var detailGenreSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
@ -451,7 +459,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
} }
} }
/** Cache of the old keys used in Auxio. */ /** Legacy keys that are no longer used, but still have to be migrated. */
private object OldKeys { private object OldKeys {
const val KEY_ACCENT3 = "auxio_accent" const val KEY_ACCENT3 = "auxio_accent"
const val KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION" const val KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION"

View file

@ -26,7 +26,7 @@ import org.oxycblt.auxio.databinding.FragmentSettingsBinding
import org.oxycblt.auxio.shared.ViewBindingFragment import org.oxycblt.auxio.shared.ViewBindingFragment
/** /**
* A container [Fragment] for the settings menu. * A [Fragment] wrapper containing the preference fragment and a companion Toolbar.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class SettingsFragment : ViewBindingFragment<FragmentSettingsBinding>() { class SettingsFragment : ViewBindingFragment<FragmentSettingsBinding>() {
@ -40,6 +40,7 @@ class SettingsFragment : ViewBindingFragment<FragmentSettingsBinding>() {
FragmentSettingsBinding.inflate(inflater) FragmentSettingsBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentSettingsBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentSettingsBinding, savedInstanceState: Bundle?) {
// Point AppBarLayout to the preference fragment's RecyclerView.
binding.settingsAppbar.liftOnScrollTargetViewId = androidx.preference.R.id.recycler_view binding.settingsAppbar.liftOnScrollTargetViewId = androidx.preference.R.id.recycler_view
binding.settingsToolbar.setNavigationOnClickListener { findNavController().navigateUp() } binding.settingsToolbar.setNavigationOnClickListener { findNavController().navigateUp() }
} }

View file

@ -105,23 +105,27 @@ private val ACCENT_PRIMARY_COLORS =
R.color.dynamic_primary) R.color.dynamic_primary)
/** /**
* The data object for an accent. In the UI this is known as a "Color Scheme." This can be nominally * The data object for a colored theme to use in the UI. This can be nominally used to gleam some
* used to gleam some attributes about a given color scheme, but this is not recommended. Attributes * attributes about a given color scheme, but this is not recommended. Attributes are the better
* are the better option in nearly all cases. * option in nearly all cases.
* *
* @property name The name of this accent * @param index The unique number for this particular accent.
* @property theme The theme resource for this accent
* @property blackTheme The black theme resource for this accent
* @property primary The primary color resource for this accent
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class Accent private constructor(val index: Int) : Item { class Accent private constructor(val index: Int) : Item {
/** The name of this [Accent]. */
val name: Int val name: Int
get() = ACCENT_NAMES[index] get() = ACCENT_NAMES[index]
/** The theme resource for this accent. */
val theme: Int val theme: Int
get() = ACCENT_THEMES[index] get() = ACCENT_THEMES[index]
/**
* The black theme resource for this accent. Identical to [theme], but with a black
* background.
*/
val blackTheme: Int val blackTheme: Int
get() = ACCENT_BLACK_THEMES[index] get() = ACCENT_BLACK_THEMES[index]
/** The accent's primary color. */
val primary: Int val primary: Int
get() = ACCENT_PRIMARY_COLORS[index] get() = ACCENT_PRIMARY_COLORS[index]
@ -130,31 +134,40 @@ class Accent private constructor(val index: Int) : Item {
override fun hashCode() = index.hashCode() override fun hashCode() = index.hashCode()
companion object { companion object {
/**
* Create a new instance.
* @param index The unique number for this particular accent.
* @return A new [Accent] with the specified [index]. If [index] is not within the
* range of valid accents, [index] will be [DEFAULT] instead.
*/
fun from(index: Int): Accent { fun from(index: Int): Accent {
if (index > (MAX - 1)) { if (index !in 0 until MAX ) {
logW("Account outside of bounds [idx: $index, max: $MAX]") logW("Accent is out of bounds [idx: $index]")
return Accent(5) return Accent(DEFAULT)
} }
return Accent(index) return Accent(index)
} }
/**
* The default accent.
*/
val DEFAULT = val DEFAULT =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Use dynamic coloring on devices that support it.
ACCENT_THEMES.lastIndex ACCENT_THEMES.lastIndex
} else { } else {
// Use blue everywhere else.
5 5
} }
/** /**
* The maximum amount of accents that are valid. This excludes the dynamic accent on * The amount of valid accents.
* versions that do not support it.
*/ */
val MAX = val MAX =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ACCENT_THEMES.size ACCENT_THEMES.size
} else { } else {
// Disable the option for a dynamic accent // Disable the option for a dynamic accent on unsupported devices.
ACCENT_THEMES.size - 1 ACCENT_THEMES.size - 1
} }
} }

View file

@ -29,11 +29,13 @@ import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
/** /**
* An adapter that displays the accent palette. * A [RecyclerView.Adapter] that displays [Accent] choices.
* @param listener A [BasicListListener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AccentAdapter(private val listener: BasicListListener) : class AccentAdapter(private val listener: BasicListListener) :
RecyclerView.Adapter<AccentViewHolder>() { RecyclerView.Adapter<AccentViewHolder>() {
/** The currently selected [Accent]. */
var selectedAccent: Accent? = null var selectedAccent: Accent? = null
private set private set
@ -42,7 +44,7 @@ class AccentAdapter(private val listener: BasicListListener) :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AccentViewHolder.new(parent) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AccentViewHolder.new(parent)
override fun onBindViewHolder(holder: AccentViewHolder, position: Int) = override fun onBindViewHolder(holder: AccentViewHolder, position: Int) =
throw IllegalStateException() throw NotImplementedError()
override fun onBindViewHolder( override fun onBindViewHolder(
holder: AccentViewHolder, holder: AccentViewHolder,
@ -50,43 +52,63 @@ class AccentAdapter(private val listener: BasicListListener) :
payloads: MutableList<Any> payloads: MutableList<Any>
) { ) {
val item = Accent.from(position) val item = Accent.from(position)
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
// Not a re-selection, re-bind with new data.
holder.bind(item, listener) holder.bind(item, listener)
} }
holder.setSelected(item == selectedAccent) holder.setSelected(item == selectedAccent)
} }
/**
* Update the currently selected [Accent].
* @param accent The new [Accent] to select.
*/
fun setSelectedAccent(accent: Accent) { fun setSelectedAccent(accent: Accent) {
if (accent == selectedAccent) return if (accent == selectedAccent) {
// Nothing to do.
return
}
// Update ViewHolders for the old selected accent and new selected accent.
selectedAccent?.let { old -> notifyItemChanged(old.index, PAYLOAD_SELECTION_CHANGED) } selectedAccent?.let { old -> notifyItemChanged(old.index, PAYLOAD_SELECTION_CHANGED) }
selectedAccent = accent selectedAccent = accent
notifyItemChanged(accent.index, PAYLOAD_SELECTION_CHANGED) notifyItemChanged(accent.index, PAYLOAD_SELECTION_CHANGED)
} }
companion object { companion object {
val PAYLOAD_SELECTION_CHANGED = Any() private val PAYLOAD_SELECTION_CHANGED = Any()
} }
} }
/**
* A [RecyclerView.ViewHolder] that displays an [Accent] choice. Use [new] to create an instance.
* @author Alexander Capehart (OxygenCobalt)
*/
class AccentViewHolder private constructor(private val binding: ItemAccentBinding) : class AccentViewHolder private constructor(private val binding: ItemAccentBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(item: Accent, listener: BasicListListener) { /**
setSelected(false) * Bind new data to this instance.
* @param accent The new [Accent] to bind.
* @param listener A [BasicListListener] to bind interactions to.
*/
fun bind(accent: Accent, listener: BasicListListener) {
binding.accent.apply { binding.accent.apply {
backgroundTintList = context.getColorCompat(item.primary) setOnClickListener { listener.onClick(accent) }
contentDescription = context.getString(item.name) backgroundTintList = context.getColorCompat(accent.primary)
// Add a Tooltip based on the content description so that the purpose of this
// button can be clear.
contentDescription = context.getString(accent.name)
TooltipCompat.setTooltipText(this, contentDescription) TooltipCompat.setTooltipText(this, contentDescription)
setOnClickListener { listener.onClick(item) }
} }
} }
/**
* Set whether this [Accent] is selected or not.
* @param isSelected Whether this [Accent] is currently selected.
*/
fun setSelected(isSelected: Boolean) { fun setSelected(isSelected: Boolean) {
binding.accent.apply { binding.accent.apply {
isEnabled = !isSelected
iconTint = iconTint =
if (isSelected) { if (isSelected) {
context.getAttrColorCompat(R.attr.colorSurface) context.getAttrColorCompat(R.attr.colorSurface)
@ -97,6 +119,11 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin
} }
companion object { companion object {
/**
* Create a new instance.
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
fun new(parent: View) = AccentViewHolder(ItemAccentBinding.inflate(parent.context.inflater)) fun new(parent: View) = AccentViewHolder(ItemAccentBinding.inflate(parent.context.inflater))
} }
} }

View file

@ -32,7 +32,7 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* Dialog responsible for showing the list of accents to select. * A [ViewBindingDialogFragment] that allows the user to configure the current [Accent].
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>(), BasicListListener { class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>(), BasicListListener {
@ -45,12 +45,14 @@ class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>(),
builder builder
.setTitle(R.string.set_accent) .setTitle(R.string.set_accent)
.setPositiveButton(R.string.lbl_ok) { _, _ -> .setPositiveButton(R.string.lbl_ok) { _, _ ->
if (accentAdapter.selectedAccent != settings.accent) { if (accentAdapter.selectedAccent == settings.accent) {
logD("Applying new accent") // Nothing to do.
settings.accent = unlikelyToBeNull(accentAdapter.selectedAccent) return@setPositiveButton
requireActivity().recreate()
} }
logD("Applying new accent")
settings.accent = unlikelyToBeNull(accentAdapter.selectedAccent)
requireActivity().recreate()
dismiss() dismiss()
} }
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
@ -58,6 +60,7 @@ class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>(),
override fun onBindingCreated(binding: DialogAccentBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogAccentBinding, savedInstanceState: Bundle?) {
binding.accentRecycler.adapter = accentAdapter binding.accentRecycler.adapter = accentAdapter
// Restore a previous pending accent if possible, otherwise select the current setting.
accentAdapter.setSelectedAccent( accentAdapter.setSelectedAccent(
if (savedInstanceState != null) { if (savedInstanceState != null) {
Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT)) Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT))
@ -68,6 +71,7 @@ class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>(),
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
// Save any pending accent configuration to restore if this dialog is re-created.
outState.putInt(KEY_PENDING_ACCENT, unlikelyToBeNull(accentAdapter.selectedAccent).index) outState.putInt(KEY_PENDING_ACCENT, unlikelyToBeNull(accentAdapter.selectedAccent).index)
} }
@ -81,6 +85,6 @@ class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>(),
} }
companion object { companion object {
const val KEY_PENDING_ACCENT = BuildConfig.APPLICATION_ID + ".key.PENDING_ACCENT" private const val KEY_PENDING_ACCENT = BuildConfig.APPLICATION_ID + ".key.PENDING_ACCENT"
} }
} }

View file

@ -26,9 +26,9 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.getDimenPixels
/** /**
* A sub-class of [GridLayoutManager] that automatically sets the spans so that they fit the width * A [GridLayoutManager] that automatically sets the span size in order to use the most possible
* of the RecyclerView. Adapted from this StackOverflow answer: * space in the [RecyclerView].
* https://stackoverflow.com/a/30256880/14143986 * Derived from this StackOverflow answer: https://stackoverflow.com/a/30256880/14143986
*/ */
class AccentGridLayoutManager( class AccentGridLayoutManager(
context: Context, context: Context,
@ -39,7 +39,6 @@ class AccentGridLayoutManager(
// We use 56dp here since that's the rough size of the accent item. // We use 56dp here since that's the rough size of the accent item.
// This will need to be modified if this is used beyond the accent dialog. // This will need to be modified if this is used beyond the accent dialog.
private var columnWidth = context.getDimenPixels(R.dimen.size_accent_item) private var columnWidth = context.getDimenPixels(R.dimen.size_accent_item)
private var lastWidth = -1 private var lastWidth = -1
private var lastHeight = -1 private var lastHeight = -1
@ -48,10 +47,8 @@ class AccentGridLayoutManager(
val totalSpace = width - paddingRight - paddingLeft val totalSpace = width - paddingRight - paddingLeft
spanCount = max(1, totalSpace / columnWidth) spanCount = max(1, totalSpace / columnWidth)
} }
lastWidth = width lastWidth = width
lastHeight = height lastHeight = height
super.onLayoutChildren(recycler, state) super.onLayoutChildren(recycler, state)
} }
} }

View file

@ -32,10 +32,10 @@ import org.oxycblt.auxio.util.getInteger
import org.oxycblt.auxio.util.lazyReflectedField import org.oxycblt.auxio.util.lazyReflectedField
/** /**
* @brief An implementation of the built-in list preference, backed with integers. * An implementation of a list-based preference backed with integers.
* *
* This is not implemented automatically, so a preference screen must override it's dialog opening * The dialog this preference corresponds to is not handled automatically, so a preference screen
* code in order to handle the preference. * must override onDisplayPreferenceDialog in order to handle it.
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@ -47,39 +47,39 @@ constructor(
defStyleAttr: Int = androidx.preference.R.attr.dialogPreferenceStyle, defStyleAttr: Int = androidx.preference.R.attr.dialogPreferenceStyle,
defStyleRes: Int = 0 defStyleRes: Int = 0
) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { ) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
/** The names of each entry. */
val entries: Array<CharSequence> val entries: Array<CharSequence>
/** The corresponding integer values for each entry. */
val values: IntArray val values: IntArray
private var offValue: Int? = -1
private var icons: TypedArray? = null
private var currentValue: Int? = null
// Reflect into Preference to get the (normally inaccessible) default value. private var entryIcons: TypedArray? = null
private val defValue: Int private var offValue: Int? = -1
get() = PREFERENCE_DEFAULT_VALUE_FIELD.get(this) as Int private var currentValue: Int? = null
init { init {
val prefAttrs = val prefAttrs =
context.obtainStyledAttributes( context.obtainStyledAttributes(
attrs, R.styleable.IntListPreference, defStyleAttr, defStyleRes) attrs, R.styleable.IntListPreference, defStyleAttr, defStyleRes)
// Can't piggy-back on ListPreference, we have to instead define our own // Can't depend on ListPreference due to it working with strings we have to instead
// attributes for entires/values. // define our own attributes for entries/values.
entries = prefAttrs.getTextArrayOrThrow(R.styleable.IntListPreference_entries) entries = prefAttrs.getTextArrayOrThrow(R.styleable.IntListPreference_entries)
values = values =
context.resources.getIntArray( context.resources.getIntArray(
prefAttrs.getResourceIdOrThrow(R.styleable.IntListPreference_entryValues)) prefAttrs.getResourceIdOrThrow(R.styleable.IntListPreference_entryValues))
// Additional values: offValue defines an "off" position // entryIcons defines an additional set of icons to use for each entry.
val iconsId = prefAttrs.getResourceId(R.styleable.IntListPreference_entryIcons, -1)
if (iconsId > -1) {
entryIcons = context.resources.obtainTypedArray(iconsId)
}
// offValue defines an value in which the preference should be disabled.
val offValueId = prefAttrs.getResourceId(R.styleable.IntListPreference_offValue, -1) val offValueId = prefAttrs.getResourceId(R.styleable.IntListPreference_offValue, -1)
if (offValueId > -1) { if (offValueId > -1) {
offValue = context.getInteger(offValueId) offValue = context.getInteger(offValueId)
} }
val iconsId = prefAttrs.getResourceId(R.styleable.IntListPreference_entryIcons, -1)
if (iconsId > -1) {
icons = context.resources.obtainTypedArray(iconsId)
}
prefAttrs.recycle() prefAttrs.recycle()
summaryProvider = IntListSummaryProvider() summaryProvider = IntListSummaryProvider()
@ -89,59 +89,73 @@ constructor(
override fun onSetInitialValue(defaultValue: Any?) { override fun onSetInitialValue(defaultValue: Any?) {
super.onSetInitialValue(defaultValue) super.onSetInitialValue(defaultValue)
if (defaultValue != null) { if (defaultValue != null) {
// If were given a default value, we need to assign it. // If were given a default value, we need to assign it.
setValue(defaultValue as Int) setValue(defaultValue as Int)
} else { } else {
currentValue = getPersistedInt(defValue) // Reflect into Preference to get the (normally inaccessible) default value.
currentValue = getPersistedInt(PREFERENCE_DEFAULT_VALUE_FIELD.get(this) as Int)
} }
} }
override fun shouldDisableDependents(): Boolean = currentValue == offValue override fun shouldDisableDependents() = currentValue == offValue
override fun onBindViewHolder(holder: PreferenceViewHolder) { override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder) super.onBindViewHolder(holder)
val index = getValueIndex() val index = getValueIndex()
if (index > -1) { if (index > -1) {
val resourceId = icons?.getResourceId(index, -1) ?: -1 // If we have a specific icon to use, make sure it is set in the view.
val resourceId = entryIcons?.getResourceId(index, -1) ?: -1
if (resourceId > -1) { if (resourceId > -1) {
(holder.findViewById(android.R.id.icon) as ImageView).setImageResource(resourceId) (holder.findViewById(android.R.id.icon) as ImageView).setImageResource(resourceId)
} }
} }
} }
/**
* Get the index of the current value.
* @return The index of the current value within [values], or -1 if the [IntListPreference]
* is not set.
*/
fun getValueIndex(): Int { fun getValueIndex(): Int {
val curValue = currentValue val curValue = currentValue
if (curValue != null) { if (curValue != null) {
return values.indexOf(curValue) return values.indexOf(curValue)
} }
return -1 return -1
} }
/** Set a value using the index of it in [values] */ /**
* Set the current value of this preference using it's index.
* @param index The index of the new value within [values]. Must be valid.
*/
fun setValueIndex(index: Int) { fun setValueIndex(index: Int) {
setValue(values[index]) setValue(values[index])
} }
private fun setValue(value: Int) { private fun setValue(value: Int) {
if (value != currentValue) { if (value != currentValue) {
currentValue = value if (!callChangeListener(value)) {
// Listener rejected the value
return
}
callChangeListener(value) // Update internal value.
currentValue = value
notifyDependencyChange(shouldDisableDependents()) notifyDependencyChange(shouldDisableDependents())
persistInt(value) persistInt(value)
notifyChanged() notifyChanged()
} }
} }
/**
* Copy of ListPreference's [Preference.SummaryProvider] for this [IntListPreference].
*/
private inner class IntListSummaryProvider : SummaryProvider<IntListPreference> { private inner class IntListSummaryProvider : SummaryProvider<IntListPreference> {
override fun provideSummary(preference: IntListPreference): CharSequence { override fun provideSummary(preference: IntListPreference): CharSequence {
val index = getValueIndex() val index = getValueIndex()
if (index != -1) { if (index != -1) {
// Get the entry corresponding to the currently shown value.
return entries[index] return entries[index]
} }

View file

@ -24,7 +24,7 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
/** /**
* @brief The companion dialog to [IntListPreference]. * The companion dialog to [IntListPreference]. Use [new] to create an instance.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class IntListPreferenceDialog : PreferenceDialogFragmentCompat() { class IntListPreferenceDialog : PreferenceDialogFragmentCompat() {
@ -34,7 +34,7 @@ class IntListPreferenceDialog : PreferenceDialogFragmentCompat() {
override fun onCreateDialog(savedInstanceState: Bundle?) = override fun onCreateDialog(savedInstanceState: Bundle?) =
// PreferenceDialogFragmentCompat does not allow us to customize the actual creation // PreferenceDialogFragmentCompat does not allow us to customize the actual creation
// of the alert dialog, so we have to manually override onCreateDialog and customize it // of the alert dialog, so we have to override onCreateDialog and create a new dialog
// ourselves. // ourselves.
MaterialAlertDialogBuilder(requireContext(), theme) MaterialAlertDialogBuilder(requireContext(), theme)
.setTitle(listPreference.title) .setTitle(listPreference.title)
@ -54,11 +54,18 @@ class IntListPreferenceDialog : PreferenceDialogFragmentCompat() {
} }
companion object { companion object {
/** The tag to use when instantiating this dialog. */
const val TAG = BuildConfig.APPLICATION_ID + ".tag.INT_PREF" const val TAG = BuildConfig.APPLICATION_ID + ".tag.INT_PREF"
fun from(pref: IntListPreference): IntListPreferenceDialog { /**
* Create a new instance.
* @param preference The [IntListPreference] to display.
* @return A new instance.
*/
fun new(preference: IntListPreference): IntListPreferenceDialog {
return IntListPreferenceDialog().apply { return IntListPreferenceDialog().apply {
arguments = Bundle().apply { putString(ARG_KEY, pref.key) } // Populate the key field required by PreferenceDialogFragmentCompat.
arguments = Bundle().apply { putString(ARG_KEY, preference.key) }
} }
} }
} }

View file

@ -19,7 +19,6 @@ package org.oxycblt.auxio.settings.prefs
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
@ -35,7 +34,6 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.settings.SettingsFragmentDirections import org.oxycblt.auxio.settings.SettingsFragmentDirections
import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -43,14 +41,12 @@ import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
/** /**
* The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat]. * The [PreferenceFragmentCompat] that displays the list of settings.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@Suppress("UNUSED")
class PreferenceFragment : PreferenceFragmentCompat() { class PreferenceFragment : PreferenceFragmentCompat() {
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val musicModel: MusicViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -58,10 +54,9 @@ class PreferenceFragment : PreferenceFragmentCompat() {
preferenceManager.onDisplayPreferenceDialogListener = this preferenceManager.onDisplayPreferenceDialogListener = this
preferenceScreen.children.forEach(::setupPreference) preferenceScreen.children.forEach(::setupPreference)
// Make the RecycleView edge-to-edge capable // Configure the RecyclerView to support edge-to-edge.
view.findViewById<RecyclerView>(androidx.preference.R.id.recycler_view).apply { view.findViewById<RecyclerView>(androidx.preference.R.id.recycler_view).apply {
clipToPadding = false clipToPadding = false
setOnApplyWindowInsetsListener { _, insets -> setOnApplyWindowInsetsListener { _, insets ->
updatePadding(bottom = insets.systemBarInsetsCompat.bottom) updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
insets insets
@ -81,21 +76,22 @@ class PreferenceFragment : PreferenceFragmentCompat() {
is IntListPreference -> { is IntListPreference -> {
// Copy the built-in preference dialog launching code into our project so // Copy the built-in preference dialog launching code into our project so
// we can automatically use the provided preference class. // we can automatically use the provided preference class.
val dialog = IntListPreferenceDialog.from(preference) val dialog = IntListPreferenceDialog.new(preference)
dialog.setTargetFragment(this, 0) dialog.setTargetFragment(this, 0)
dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG) dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG)
} }
is WrappedDialogPreference -> { is WrappedDialogPreference -> {
val context = requireContext() // WrappedDialogPreference cannot launch a dialog on it's own, it has to
// be handled manually.
val directions = val directions =
when (preference.key) { when (preference.key) {
context.getString(R.string.set_key_accent) -> getString(R.string.set_key_accent) ->
SettingsFragmentDirections.goToAccentDialog() SettingsFragmentDirections.goToAccentDialog()
context.getString(R.string.set_key_lib_tabs) -> getString(R.string.set_key_lib_tabs) ->
SettingsFragmentDirections.goToTabDialog() SettingsFragmentDirections.goToTabDialog()
context.getString(R.string.set_key_pre_amp) -> getString(R.string.set_key_pre_amp) ->
SettingsFragmentDirections.goToPreAmpDialog() SettingsFragmentDirections.goToPreAmpDialog()
context.getString(R.string.set_key_music_dirs) -> getString(R.string.set_key_music_dirs) ->
SettingsFragmentDirections.goToMusicDirsDialog() SettingsFragmentDirections.goToMusicDirsDialog()
getString(R.string.set_key_separators) -> getString(R.string.set_key_separators) ->
SettingsFragmentDirections.goToSeparatorsDialog() SettingsFragmentDirections.goToSeparatorsDialog()
@ -110,6 +106,9 @@ class PreferenceFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
val context = requireContext() val context = requireContext()
// Hook generic preferences to their specified preferences
// TODO: These seem like good things to put into a side navigation view, if I choose to
// do one.
when (preference.key) { when (preference.key) {
context.getString(R.string.set_key_save_state) -> { context.getString(R.string.set_key_save_state) -> {
playbackModel.savePlaybackState { saved -> playbackModel.savePlaybackState { saved ->
@ -125,6 +124,8 @@ class PreferenceFragment : PreferenceFragmentCompat() {
context.getString(R.string.set_key_wipe_state) -> { context.getString(R.string.set_key_wipe_state) -> {
playbackModel.wipePlaybackState { wiped -> playbackModel.wipePlaybackState { wiped ->
if (wiped) { if (wiped) {
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
this.context?.showToast(R.string.lbl_state_wiped) this.context?.showToast(R.string.lbl_state_wiped)
} else { } else {
this.context?.showToast(R.string.err_did_not_wipe) this.context?.showToast(R.string.err_did_not_wipe)
@ -134,6 +135,8 @@ class PreferenceFragment : PreferenceFragmentCompat() {
context.getString(R.string.set_key_restore_state) -> context.getString(R.string.set_key_restore_state) ->
playbackModel.tryRestorePlaybackState { restored -> playbackModel.tryRestorePlaybackState { restored ->
if (restored) { if (restored) {
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
this.context?.showToast(R.string.lbl_state_restored) this.context?.showToast(R.string.lbl_state_restored)
} else { } else {
this.context?.showToast(R.string.err_did_not_restore) this.context?.showToast(R.string.err_did_not_restore)
@ -151,7 +154,10 @@ class PreferenceFragment : PreferenceFragmentCompat() {
val context = requireActivity() val context = requireActivity()
val settings = Settings(context) val settings = Settings(context)
if (!preference.isVisible) return if (!preference.isVisible) {
// Nothing to do.
return
}
if (preference is PreferenceCategory) { if (preference is PreferenceCategory) {
preference.children.forEach(::setupPreference) preference.children.forEach(::setupPreference)
@ -188,15 +194,4 @@ class PreferenceFragment : PreferenceFragmentCompat() {
} }
} }
} }
/** Convert an theme integer into an icon that can be used. */
@DrawableRes
private fun Int.toThemeIcon(): Int {
return when (this) {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> R.drawable.ic_auto_24
AppCompatDelegate.MODE_NIGHT_NO -> R.drawable.ic_light_24
AppCompatDelegate.MODE_NIGHT_YES -> R.drawable.ic_dark_24
else -> R.drawable.ic_auto_24
}
}
} }

View file

@ -22,8 +22,9 @@ import android.util.AttributeSet
import androidx.preference.DialogPreference import androidx.preference.DialogPreference
/** /**
* Wraps [DialogPreference] as to make it type-distinct from other preferences while also making it * Wraps a [DialogPreference] to be instantiatable. This has no purpose other to ensure that
* possible to use in a PreferenceScreen. * custom dialog preferences are handled.
* @author Alexander Capehart (OxygenCobalt)
*/ */
class WrappedDialogPreference class WrappedDialogPreference
@JvmOverloads @JvmOverloads