settings: decouple
Decouple the settings god object into feature-specific settings. This should make testing settings-dependent code much easier, as it no longer requires a context.
This commit is contained in:
parent
3502af33e7
commit
1b19b698a1
51 changed files with 1038 additions and 597 deletions
|
@ -18,7 +18,9 @@
|
|||
package org.oxycblt.auxio
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
|
@ -28,5 +30,10 @@ import org.junit.runner.RunWith
|
|||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class StubTest {
|
||||
// TODO: Add tests
|
||||
// TODO: Make tests
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("org.oxycblt.auxio", appContext.packageName)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,12 +25,14 @@ import androidx.core.graphics.drawable.IconCompat
|
|||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.request.CachePolicy
|
||||
import org.oxycblt.auxio.image.ImageSettings
|
||||
import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher
|
||||
import org.oxycblt.auxio.image.extractor.ArtistImageFetcher
|
||||
import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFactory
|
||||
import org.oxycblt.auxio.image.extractor.GenreImageFetcher
|
||||
import org.oxycblt.auxio.image.extractor.MusicKeyer
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.ui.UISettings
|
||||
|
||||
/**
|
||||
* Auxio: A simple, rational music player for android.
|
||||
|
@ -40,7 +42,9 @@ class AuxioApp : Application(), ImageLoaderFactory {
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// Migrate any settings that may have changed in an app update.
|
||||
Settings(this).migrate()
|
||||
ImageSettings.from(this).migrate()
|
||||
PlaybackSettings.from(this).migrate()
|
||||
UISettings.from(this).migrate()
|
||||
// Adding static shortcuts in a dynamic manner is better than declaring them
|
||||
// manually, as it will properly handle the difference between debug and release
|
||||
// Auxio instances.
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.system.IndexerService
|
|||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||
import org.oxycblt.auxio.playback.system.PlaybackService
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.UISettings
|
||||
import org.oxycblt.auxio.util.androidViewModels
|
||||
import org.oxycblt.auxio.util.isNight
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -81,7 +81,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun setupTheme() {
|
||||
val settings = Settings(this)
|
||||
val settings = UISettings.from(this)
|
||||
// Apply the theme configuration.
|
||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||
// Apply the color scheme. The black theme requires it's own set of themes since
|
||||
|
|
|
@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.MusicMode
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.util.*
|
||||
|
||||
/**
|
||||
|
@ -123,7 +123,7 @@ class AlbumDetailFragment :
|
|||
|
||||
override fun onRealClick(item: Music) {
|
||||
val song = requireIs<Song>(item)
|
||||
when (Settings(requireContext()).detailPlaybackMode) {
|
||||
when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) {
|
||||
// "Play from shown item" and "Play from album" functionally have the same
|
||||
// behavior since a song can only have one album.
|
||||
null,
|
||||
|
@ -149,12 +149,12 @@ class AlbumDetailFragment :
|
|||
|
||||
override fun onOpenSortMenu(anchor: View) {
|
||||
openMenu(anchor, R.menu.menu_album_sort) {
|
||||
val sort = detailModel.albumSort
|
||||
val sort = detailModel.albumSortSort
|
||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||
setOnMenuItemClickListener { item ->
|
||||
item.isChecked = !item.isChecked
|
||||
detailModel.albumSort =
|
||||
detailModel.albumSortSort =
|
||||
if (item.itemId == R.id.option_sort_asc) {
|
||||
sort.withAscending(item.isChecked)
|
||||
} else {
|
||||
|
|
|
@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.MusicMode
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.util.collect
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -123,7 +123,7 @@ class ArtistDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detai
|
|||
override fun onRealClick(item: Music) {
|
||||
when (item) {
|
||||
is Song -> {
|
||||
when (Settings(requireContext()).detailPlaybackMode) {
|
||||
when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) {
|
||||
// When configured to play from the selected item, we already have an Artist
|
||||
// to play from.
|
||||
null ->
|
||||
|
@ -158,13 +158,13 @@ class ArtistDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detai
|
|||
|
||||
override fun onOpenSortMenu(anchor: View) {
|
||||
openMenu(anchor, R.menu.menu_artist_sort) {
|
||||
val sort = detailModel.artistSort
|
||||
val sort = detailModel.artistSongSort
|
||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||
setOnMenuItemClickListener { item ->
|
||||
item.isChecked = !item.isChecked
|
||||
|
||||
detailModel.artistSort =
|
||||
detailModel.artistSongSort =
|
||||
if (item.itemId == R.id.option_sort_asc) {
|
||||
sort.withAscending(item.isChecked)
|
||||
} else {
|
||||
|
|
|
@ -33,8 +33,10 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.list.Header
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.Library
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.music.storage.MimeType
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.*
|
||||
|
||||
/**
|
||||
|
@ -47,7 +49,7 @@ import org.oxycblt.auxio.util.*
|
|||
class DetailViewModel(application: Application) :
|
||||
AndroidViewModel(application), MusicStore.Listener {
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
private val settings = Settings(application)
|
||||
private val musicSettings = MusicSettings.from(application)
|
||||
|
||||
private var currentSongJob: Job? = null
|
||||
|
||||
|
@ -75,10 +77,10 @@ class DetailViewModel(application: Application) :
|
|||
get() = _albumList
|
||||
|
||||
/** The current [Sort] used for [Song]s in [albumList]. */
|
||||
var albumSort: Sort
|
||||
get() = settings.detailAlbumSort
|
||||
var albumSortSort: Sort
|
||||
get() = musicSettings.albumSongSort
|
||||
set(value) {
|
||||
settings.detailAlbumSort = value
|
||||
musicSettings.albumSongSort = value
|
||||
// Refresh the album list to reflect the new sort.
|
||||
currentAlbum.value?.let(::refreshAlbumList)
|
||||
}
|
||||
|
@ -95,10 +97,10 @@ class DetailViewModel(application: Application) :
|
|||
val artistList: StateFlow<List<Item>> = _artistList
|
||||
|
||||
/** The current [Sort] used for [Song]s in [artistList]. */
|
||||
var artistSort: Sort
|
||||
get() = settings.detailArtistSort
|
||||
var artistSongSort: Sort
|
||||
get() = musicSettings.artistSongSort
|
||||
set(value) {
|
||||
settings.detailArtistSort = value
|
||||
musicSettings.artistSongSort = value
|
||||
// Refresh the artist list to reflect the new sort.
|
||||
currentArtist.value?.let(::refreshArtistList)
|
||||
}
|
||||
|
@ -115,10 +117,10 @@ class DetailViewModel(application: Application) :
|
|||
val genreList: StateFlow<List<Item>> = _genreList
|
||||
|
||||
/** The current [Sort] used for [Song]s in [genreList]. */
|
||||
var genreSort: Sort
|
||||
get() = settings.detailGenreSort
|
||||
var genreSongSort: Sort
|
||||
get() = musicSettings.genreSongSort
|
||||
set(value) {
|
||||
settings.detailGenreSort = value
|
||||
musicSettings.genreSongSort = value
|
||||
// Refresh the genre list to reflect the new sort.
|
||||
currentGenre.value?.let(::refreshGenreList)
|
||||
}
|
||||
|
@ -309,7 +311,7 @@ class DetailViewModel(application: Application) :
|
|||
|
||||
// To create a good user experience regarding disc numbers, we group the album's
|
||||
// songs up by disc and then delimit the groups by a disc header.
|
||||
val songs = albumSort.songs(album.songs)
|
||||
val songs = albumSortSort.songs(album.songs)
|
||||
// Songs without disc tags become part of Disc 1.
|
||||
val byDisc = songs.groupBy { it.disc ?: 1 }
|
||||
if (byDisc.size > 1) {
|
||||
|
@ -363,7 +365,7 @@ class DetailViewModel(application: Application) :
|
|||
if (artist.songs.isNotEmpty()) {
|
||||
logD("Songs present in this artist, adding header")
|
||||
data.add(SortHeader(R.string.lbl_songs))
|
||||
data.addAll(artistSort.songs(artist.songs))
|
||||
data.addAll(artistSongSort.songs(artist.songs))
|
||||
}
|
||||
|
||||
_artistList.value = data.toList()
|
||||
|
@ -376,7 +378,7 @@ class DetailViewModel(application: Application) :
|
|||
data.add(Header(R.string.lbl_artists))
|
||||
data.addAll(genre.artists)
|
||||
data.add(SortHeader(R.string.lbl_songs))
|
||||
data.addAll(genreSort.songs(genre.songs))
|
||||
data.addAll(genreSongSort.songs(genre.songs))
|
||||
_genreList.value = data
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ import org.oxycblt.auxio.music.MusicMode
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.util.collect
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -123,7 +123,7 @@ class GenreDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detail
|
|||
when (item) {
|
||||
is Artist -> navModel.exploreNavigateTo(item)
|
||||
is Song ->
|
||||
when (Settings(requireContext()).detailPlaybackMode) {
|
||||
when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) {
|
||||
// When configured to play from the selected item, we already have a Genre
|
||||
// to play from.
|
||||
null ->
|
||||
|
@ -156,12 +156,12 @@ class GenreDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detail
|
|||
|
||||
override fun onOpenSortMenu(anchor: View) {
|
||||
openMenu(anchor, R.menu.menu_genre_sort) {
|
||||
val sort = detailModel.genreSort
|
||||
val sort = detailModel.genreSongSort
|
||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||
setOnMenuItemClickListener { item ->
|
||||
item.isChecked = !item.isChecked
|
||||
detailModel.genreSort =
|
||||
detailModel.genreSongSort =
|
||||
if (item.itemId == R.id.option_sort_asc) {
|
||||
sort.withAscending(item.isChecked)
|
||||
} else {
|
||||
|
|
|
@ -50,6 +50,8 @@ import org.oxycblt.auxio.home.list.SongListFragment
|
|||
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
|
||||
import org.oxycblt.auxio.list.selection.SelectionFragment
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.Library
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.music.system.Indexer
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
|
|
64
app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt
Normal file
64
app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.home
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.home.tabs.Tab
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
/**
|
||||
* User configuration specific to the home UI.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface HomeSettings : Settings {
|
||||
/** The tabs to show in the home UI. */
|
||||
var homeTabs: Array<Tab>
|
||||
/** Whether to hide artists considered "collaborators" from the home UI. */
|
||||
val shouldHideCollaborators: Boolean
|
||||
|
||||
private class Real(context: Context) : Settings.Real(context), HomeSettings {
|
||||
override var homeTabs: Array<Tab>
|
||||
get() =
|
||||
Tab.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_lib_tabs), Tab.SEQUENCE_DEFAULT))
|
||||
?: unlikelyToBeNull(Tab.fromIntCode(Tab.SEQUENCE_DEFAULT))
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_tabs), Tab.toIntCode(value))
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override val shouldHideCollaborators: Boolean
|
||||
get() =
|
||||
sharedPreferences.getBoolean(
|
||||
context.getString(R.string.set_key_hide_collaborators), false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Get a framework-backed implementation.
|
||||
* @param context [Context] required.
|
||||
*/
|
||||
fun from(context: Context): HomeSettings = Real(context)
|
||||
}
|
||||
}
|
|
@ -25,7 +25,9 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.home.tabs.Tab
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.music.Library
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -38,7 +40,8 @@ class HomeViewModel(application: Application) :
|
|||
MusicStore.Listener,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
private val settings = Settings(application)
|
||||
private val homeSettings = HomeSettings.from(application)
|
||||
private val musicSettings = MusicSettings.from(application)
|
||||
|
||||
private val _songsList = MutableStateFlow(listOf<Song>())
|
||||
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
|
||||
|
@ -89,13 +92,13 @@ class HomeViewModel(application: Application) :
|
|||
|
||||
init {
|
||||
musicStore.addListener(this)
|
||||
settings.addListener(this)
|
||||
homeSettings.addListener(this)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
musicStore.removeListener(this)
|
||||
settings.removeListener(this)
|
||||
homeSettings.removeListener(this)
|
||||
}
|
||||
|
||||
override fun onLibraryChanged(library: Library?) {
|
||||
|
@ -103,17 +106,17 @@ class HomeViewModel(application: Application) :
|
|||
logD("Library changed, refreshing library")
|
||||
// Get the each list of items in the library to use as our list data.
|
||||
// Applying the preferred sorting to them.
|
||||
_songsList.value = settings.libSongSort.songs(library.songs)
|
||||
_albumsLists.value = settings.libAlbumSort.albums(library.albums)
|
||||
_songsList.value = musicSettings.songSort.songs(library.songs)
|
||||
_albumsLists.value = musicSettings.albumSort.albums(library.albums)
|
||||
_artistsList.value =
|
||||
settings.libArtistSort.artists(
|
||||
if (settings.shouldHideCollaborators) {
|
||||
musicSettings.artistSort.artists(
|
||||
if (homeSettings.shouldHideCollaborators) {
|
||||
// Hide Collaborators is enabled, filter out collaborators.
|
||||
library.artists.filter { !it.isCollaborator }
|
||||
} else {
|
||||
library.artists
|
||||
})
|
||||
_genresList.value = settings.libGenreSort.genres(library.genres)
|
||||
_genresList.value = musicSettings.genreSort.genres(library.genres)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,10 +159,10 @@ class HomeViewModel(application: Application) :
|
|||
*/
|
||||
fun getSortForTab(tabMode: MusicMode) =
|
||||
when (tabMode) {
|
||||
MusicMode.SONGS -> settings.libSongSort
|
||||
MusicMode.ALBUMS -> settings.libAlbumSort
|
||||
MusicMode.ARTISTS -> settings.libArtistSort
|
||||
MusicMode.GENRES -> settings.libGenreSort
|
||||
MusicMode.SONGS -> musicSettings.songSort
|
||||
MusicMode.ALBUMS -> musicSettings.albumSort
|
||||
MusicMode.ARTISTS -> musicSettings.artistSort
|
||||
MusicMode.GENRES -> musicSettings.genreSort
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -171,19 +174,19 @@ class HomeViewModel(application: Application) :
|
|||
// Can simply re-sort the current list of items without having to access the library.
|
||||
when (_currentTabMode.value) {
|
||||
MusicMode.SONGS -> {
|
||||
settings.libSongSort = sort
|
||||
musicSettings.songSort = sort
|
||||
_songsList.value = sort.songs(_songsList.value)
|
||||
}
|
||||
MusicMode.ALBUMS -> {
|
||||
settings.libAlbumSort = sort
|
||||
musicSettings.albumSort = sort
|
||||
_albumsLists.value = sort.albums(_albumsLists.value)
|
||||
}
|
||||
MusicMode.ARTISTS -> {
|
||||
settings.libArtistSort = sort
|
||||
musicSettings.artistSort = sort
|
||||
_artistsList.value = sort.artists(_artistsList.value)
|
||||
}
|
||||
MusicMode.GENRES -> {
|
||||
settings.libGenreSort = sort
|
||||
musicSettings.genreSort = sort
|
||||
_genresList.value = sort.genres(_genresList.value)
|
||||
}
|
||||
}
|
||||
|
@ -203,5 +206,6 @@ class HomeViewModel(application: Application) :
|
|||
* @return A list of the [MusicMode]s for each visible [Tab] in the configuration, ordered in
|
||||
* the same way as the configuration.
|
||||
*/
|
||||
private fun makeTabModes() = settings.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
||||
private fun makeTabModes() =
|
||||
homeSettings.homeTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.oxycblt.auxio.list.recycler.AlbumViewHolder
|
|||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.playback.secsToMs
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
|
|
@ -37,9 +37,9 @@ import org.oxycblt.auxio.music.MusicMode
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.playback.secsToMs
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
||||
/**
|
||||
|
@ -130,7 +130,7 @@ class SongListFragment :
|
|||
}
|
||||
|
||||
override fun onRealClick(item: Song) {
|
||||
when (Settings(requireContext()).libPlaybackMode) {
|
||||
when (PlaybackSettings.from(requireContext()).inListPlaybackMode) {
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||
|
|
|
@ -25,8 +25,8 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogTabsBinding
|
||||
import org.oxycblt.auxio.home.HomeSettings
|
||||
import org.oxycblt.auxio.list.EditableListListener
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -46,13 +46,13 @@ class TabCustomizeDialog :
|
|||
.setTitle(R.string.set_lib_tabs)
|
||||
.setPositiveButton(R.string.lbl_ok) { _, _ ->
|
||||
logD("Committing tab changes")
|
||||
Settings(requireContext()).libTabs = tabAdapter.tabs
|
||||
HomeSettings.from(requireContext()).homeTabs = tabAdapter.tabs
|
||||
}
|
||||
.setNegativeButton(R.string.lbl_cancel, null)
|
||||
}
|
||||
|
||||
override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) {
|
||||
var tabs = Settings(requireContext()).libTabs
|
||||
var tabs = HomeSettings.from(requireContext()).homeTabs
|
||||
// Try to restore a pending tab configuration that was saved prior.
|
||||
if (savedInstanceState != null) {
|
||||
val savedTabs = Tab.fromIntCode(savedInstanceState.getInt(KEY_TABS))
|
||||
|
|
77
app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt
Normal file
77
app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.image
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* User configuration specific to image loading.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface ImageSettings : Settings {
|
||||
/** The strategy to use when loading album covers. */
|
||||
val coverMode: CoverMode
|
||||
|
||||
private class Real(context: Context) : Settings.Real(context), ImageSettings {
|
||||
override val coverMode: CoverMode
|
||||
get() =
|
||||
CoverMode.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
|
||||
?: CoverMode.MEDIA_STORE
|
||||
|
||||
override fun migrate() {
|
||||
// Show album covers and Ignore MediaStore covers were unified in 3.0.0
|
||||
if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) ||
|
||||
sharedPreferences.contains(OLD_KEY_QUALITY_COVERS)) {
|
||||
logD("Migrating cover settings")
|
||||
|
||||
val mode =
|
||||
when {
|
||||
!sharedPreferences.getBoolean(OLD_KEY_SHOW_COVERS, true) -> CoverMode.OFF
|
||||
!sharedPreferences.getBoolean(OLD_KEY_QUALITY_COVERS, true) ->
|
||||
CoverMode.MEDIA_STORE
|
||||
else -> CoverMode.QUALITY
|
||||
}
|
||||
|
||||
sharedPreferences.edit {
|
||||
putInt(context.getString(R.string.set_key_cover_mode), mode.intCode)
|
||||
remove(OLD_KEY_SHOW_COVERS)
|
||||
remove(OLD_KEY_QUALITY_COVERS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val OLD_KEY_SHOW_COVERS = "KEY_SHOW_COVERS"
|
||||
const val OLD_KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Get a framework-backed implementation.
|
||||
* @param context [Context] required.
|
||||
*/
|
||||
fun from(context: Context): ImageSettings = Real(context)
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ import androidx.core.widget.ImageViewCompat
|
|||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import kotlin.math.max
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.UISettings
|
||||
import org.oxycblt.auxio.util.getColorCompat
|
||||
import org.oxycblt.auxio.util.getDrawableCompat
|
||||
|
||||
|
@ -52,7 +52,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
private val indicatorMatrix = Matrix()
|
||||
private val indicatorMatrixSrc = RectF()
|
||||
private val indicatorMatrixDst = RectF()
|
||||
private val settings = Settings(context)
|
||||
|
||||
/**
|
||||
* The corner radius of this view. This allows the outer ImageGroup to apply it's corner radius
|
||||
|
@ -62,7 +61,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
set(value) {
|
||||
field = value
|
||||
(background as? MaterialShapeDrawable)?.let { bg ->
|
||||
if (settings.roundMode) {
|
||||
if (UISettings.from(context).roundMode) {
|
||||
bg.setCornerSize(value)
|
||||
} else {
|
||||
bg.setCornerSize(0f)
|
||||
|
|
|
@ -39,7 +39,7 @@ import org.oxycblt.auxio.music.Artist
|
|||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.UISettings
|
||||
import org.oxycblt.auxio.util.getColorCompat
|
||||
import org.oxycblt.auxio.util.getDrawableCompat
|
||||
|
||||
|
@ -81,7 +81,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
background =
|
||||
MaterialShapeDrawable().apply {
|
||||
fillColor = context.getColorCompat(R.color.sel_cover_bg)
|
||||
if (Settings(context).roundMode) {
|
||||
if (UISettings.from(context).roundMode) {
|
||||
// Only use the specified corner radius when round mode is enabled.
|
||||
setCornerSize(cornerRadius)
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ import java.io.InputStream
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.image.CoverMode
|
||||
import org.oxycblt.auxio.image.ImageSettings
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
|
@ -47,10 +47,8 @@ object Covers {
|
|||
* loading failed or should not occur.
|
||||
*/
|
||||
suspend fun fetch(context: Context, album: Album): InputStream? {
|
||||
val settings = Settings(context)
|
||||
|
||||
return try {
|
||||
when (settings.coverMode) {
|
||||
when (ImageSettings.from(context).coverMode) {
|
||||
CoverMode.OFF -> null
|
||||
CoverMode.MEDIA_STORE -> fetchMediaStoreCovers(context, album)
|
||||
CoverMode.QUALITY -> fetchQualityCovers(context, album)
|
||||
|
|
|
@ -21,6 +21,8 @@ import androidx.lifecycle.ViewModel
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.Library
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
|
||||
/**
|
||||
* A [ViewModel] that manages the current selection.
|
||||
|
|
|
@ -20,9 +20,9 @@ package org.oxycblt.auxio.music
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.storage.contentResolverSafe
|
||||
import org.oxycblt.auxio.music.storage.useQuery
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
|
@ -33,7 +33,7 @@ import org.oxycblt.auxio.util.logD
|
|||
*
|
||||
* @author Alexander Capehart
|
||||
*/
|
||||
class Library(rawSongs: List<Song.Raw>, settings: Settings) {
|
||||
class Library(rawSongs: List<Song.Raw>, settings: MusicSettings) {
|
||||
/** All [Song]s that were detected on the device. */
|
||||
val songs = Sort(Sort.Mode.ByName, true).songs(rawSongs.map { Song(it, settings) })
|
||||
/** All [Album]s found on the device. */
|
||||
|
|
|
@ -33,7 +33,6 @@ import org.oxycblt.auxio.list.Item
|
|||
import org.oxycblt.auxio.music.parsing.parseId3GenreNames
|
||||
import org.oxycblt.auxio.music.parsing.parseMultiValue
|
||||
import org.oxycblt.auxio.music.storage.*
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
|
@ -308,10 +307,10 @@ sealed class MusicParent : Music() {
|
|||
/**
|
||||
* A song. Perhaps the foundation of the entirety of Auxio.
|
||||
* @param raw The [Song.Raw] to derive the member data from.
|
||||
* @param settings [Settings] to determine the artist configuration.
|
||||
* @param musicSettings [MusicSettings] to perform further user-configured parsing.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||
class Song constructor(raw: Raw, musicSettings: MusicSettings) : Music() {
|
||||
override val uid =
|
||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||
raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) }
|
||||
|
@ -381,10 +380,9 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
val album: Album
|
||||
get() = unlikelyToBeNull(_album)
|
||||
|
||||
private val artistMusicBrainzIds =
|
||||
raw.artistMusicBrainzIds.parseMultiValue(settings.musicSeparators)
|
||||
private val artistNames = raw.artistNames.parseMultiValue(settings.musicSeparators)
|
||||
private val artistSortNames = raw.artistSortNames.parseMultiValue(settings.musicSeparators)
|
||||
private val artistMusicBrainzIds = raw.artistMusicBrainzIds.parseMultiValue(musicSettings)
|
||||
private val artistNames = raw.artistNames.parseMultiValue(musicSettings)
|
||||
private val artistSortNames = raw.artistSortNames.parseMultiValue(musicSettings)
|
||||
private val rawArtists =
|
||||
artistNames.mapIndexed { i, name ->
|
||||
Artist.Raw(
|
||||
|
@ -394,10 +392,9 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
}
|
||||
|
||||
private val albumArtistMusicBrainzIds =
|
||||
raw.albumArtistMusicBrainzIds.parseMultiValue(settings.musicSeparators)
|
||||
private val albumArtistNames = raw.albumArtistNames.parseMultiValue(settings.musicSeparators)
|
||||
private val albumArtistSortNames =
|
||||
raw.albumArtistSortNames.parseMultiValue(settings.musicSeparators)
|
||||
raw.albumArtistMusicBrainzIds.parseMultiValue(musicSettings)
|
||||
private val albumArtistNames = raw.albumArtistNames.parseMultiValue(musicSettings)
|
||||
private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(musicSettings)
|
||||
private val rawAlbumArtists =
|
||||
albumArtistNames.mapIndexed { i, name ->
|
||||
Artist.Raw(
|
||||
|
@ -465,7 +462,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
|
||||
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
||||
sortName = raw.albumSortName,
|
||||
type = Album.Type.parse(raw.albumTypes.parseMultiValue(settings.musicSeparators)),
|
||||
type = Album.Type.parse(raw.albumTypes.parseMultiValue(musicSettings)),
|
||||
rawArtists =
|
||||
rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
|
||||
|
||||
|
@ -484,7 +481,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
*/
|
||||
val _rawGenres =
|
||||
raw.genreNames
|
||||
.parseId3GenreNames(settings.musicSeparators)
|
||||
.parseId3GenreNames(musicSettings)
|
||||
.map { Genre.Raw(it) }
|
||||
.ifEmpty { listOf(Genre.Raw()) }
|
||||
|
||||
|
|
213
app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt
Normal file
213
app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt
Normal file
|
@ -0,0 +1,213 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music
|
||||
|
||||
import android.content.Context
|
||||
import android.os.storage.StorageManager
|
||||
import androidx.core.content.edit
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.storage.Directory
|
||||
import org.oxycblt.auxio.music.storage.MusicDirectories
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
|
||||
/**
|
||||
* User configuration specific to music system.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface MusicSettings : Settings {
|
||||
/** The configuration on how to handle particular directories in the music library. */
|
||||
var musicDirs: MusicDirectories
|
||||
/** Whether to exclude non-music audio files from the music library. */
|
||||
val excludeNonMusic: Boolean
|
||||
/** Whether to be actively watching for changes in the music library. */
|
||||
val shouldBeObserving: Boolean
|
||||
/** A [String] of characters representing the desired characters to denote multi-value tags. */
|
||||
var multiValueSeparators: String
|
||||
/** The [Sort] mode used in [Song] lists. */
|
||||
var songSort: Sort
|
||||
/** The [Sort] mode used in [Album] lists. */
|
||||
var albumSort: Sort
|
||||
/** The [Sort] mode used in [Artist] lists. */
|
||||
var artistSort: Sort
|
||||
/** The [Sort] mode used in [Genre] lists. */
|
||||
var genreSort: Sort
|
||||
/** The [Sort] mode used in an [Album]'s [Song] list. */
|
||||
var albumSongSort: Sort
|
||||
/** The [Sort] mode used in an [Artist]'s [Song] list. */
|
||||
var artistSongSort: Sort
|
||||
/** The [Sort] mode used in an [Genre]'s [Song] list. */
|
||||
var genreSongSort: Sort
|
||||
|
||||
private class Real(context: Context) : Settings.Real(context), MusicSettings {
|
||||
private val storageManager = context.getSystemServiceCompat(StorageManager::class)
|
||||
|
||||
override var musicDirs: MusicDirectories
|
||||
get() {
|
||||
val dirs =
|
||||
(sharedPreferences.getStringSet(
|
||||
context.getString(R.string.set_key_music_dirs), null)
|
||||
?: emptySet())
|
||||
.mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) }
|
||||
return MusicDirectories(
|
||||
dirs,
|
||||
sharedPreferences.getBoolean(
|
||||
context.getString(R.string.set_key_music_dirs_include), false))
|
||||
}
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putStringSet(
|
||||
context.getString(R.string.set_key_music_dirs),
|
||||
value.dirs.map(Directory::toDocumentTreeUri).toSet())
|
||||
putBoolean(
|
||||
context.getString(R.string.set_key_music_dirs_include), value.shouldInclude)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override val excludeNonMusic: Boolean
|
||||
get() =
|
||||
sharedPreferences.getBoolean(
|
||||
context.getString(R.string.set_key_exclude_non_music), true)
|
||||
|
||||
override val shouldBeObserving: Boolean
|
||||
get() =
|
||||
sharedPreferences.getBoolean(context.getString(R.string.set_key_observing), false)
|
||||
|
||||
override var multiValueSeparators: String
|
||||
// Differ from convention and store a string of separator characters instead of an int
|
||||
// code. This makes it easier to use and more extendable.
|
||||
get() =
|
||||
sharedPreferences.getString(context.getString(R.string.set_key_separators), "")
|
||||
?: ""
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putString(context.getString(R.string.set_key_separators), value)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override var songSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_songs_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override var albumSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_albums_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override var artistSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override var genreSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_genres_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override var albumSongSort: Sort
|
||||
get() {
|
||||
var sort =
|
||||
Sort.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByDisc, true)
|
||||
|
||||
// Correct legacy album sort modes to Disc
|
||||
if (sort.mode is Sort.Mode.ByName) {
|
||||
sort = sort.withMode(Sort.Mode.ByDisc)
|
||||
}
|
||||
|
||||
return sort
|
||||
}
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putInt(context.getString(R.string.set_key_detail_album_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override var artistSongSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByDate, false)
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putInt(context.getString(R.string.set_key_detail_artist_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override var genreSongSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putInt(context.getString(R.string.set_key_detail_genre_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Get a framework-backed implementation.
|
||||
* @param context [Context] required.
|
||||
*/
|
||||
fun from(context: Context): MusicSettings = Real(context)
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ import androidx.core.database.getIntOrNull
|
|||
import androidx.core.database.getStringOrNull
|
||||
import java.io.File
|
||||
import org.oxycblt.auxio.music.Date
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.parsing.parseId3v2Position
|
||||
import org.oxycblt.auxio.music.storage.Directory
|
||||
|
@ -37,7 +38,6 @@ import org.oxycblt.auxio.music.storage.mediaStoreVolumeNameCompat
|
|||
import org.oxycblt.auxio.music.storage.safeQuery
|
||||
import org.oxycblt.auxio.music.storage.storageVolumesCompat
|
||||
import org.oxycblt.auxio.music.storage.useQuery
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
|
@ -86,20 +86,20 @@ abstract class MediaStoreExtractor(
|
|||
open fun init(): Cursor {
|
||||
val start = System.currentTimeMillis()
|
||||
cacheExtractor.init()
|
||||
val settings = Settings(context)
|
||||
val musicSettings = MusicSettings.from(context)
|
||||
val storageManager = context.getSystemServiceCompat(StorageManager::class)
|
||||
|
||||
val args = mutableListOf<String>()
|
||||
var selector = BASE_SELECTOR
|
||||
|
||||
// Filter out audio that is not music, if enabled.
|
||||
if (settings.excludeNonMusic) {
|
||||
if (musicSettings.excludeNonMusic) {
|
||||
logD("Excluding non-music")
|
||||
selector += " AND ${MediaStore.Audio.AudioColumns.IS_MUSIC}=1"
|
||||
}
|
||||
|
||||
// Set up the projection to follow the music directory configuration.
|
||||
val dirs = settings.getMusicDirs(storageManager)
|
||||
val dirs = musicSettings.musicDirs
|
||||
if (dirs.dirs.isNotEmpty()) {
|
||||
selector += " AND "
|
||||
if (!dirs.shouldInclude) {
|
||||
|
|
|
@ -230,7 +230,7 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
* Frames.
|
||||
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
|
||||
* values.
|
||||
* @retrn A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a
|
||||
* @return A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a
|
||||
* hour/minute value from TIME. No second value is included. The latter two fields may not be
|
||||
* included in they cannot be parsed. Will be null if a year value could not be parsed.
|
||||
*/
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.oxycblt.auxio.music.parsing
|
||||
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
|
||||
/// --- GENERIC PARSING ---
|
||||
|
@ -25,12 +26,12 @@ import org.oxycblt.auxio.util.nonZeroOrNull
|
|||
* Parse a multi-value tag based on the user configuration. If the value is already composed of more
|
||||
* than one value, nothing is done. Otherwise, this function will attempt to split it based on the
|
||||
* user's separator preferences.
|
||||
* @param separators A string of characters to split by. Can be empty.
|
||||
* @param settings [MusicSettings] required to obtain user separator configuration.
|
||||
* @return A new list of one or more [String]s.
|
||||
*/
|
||||
fun List<String>.parseMultiValue(separators: String) =
|
||||
fun List<String>.parseMultiValue(settings: MusicSettings) =
|
||||
if (size == 1) {
|
||||
first().maybeParseBySeparators(separators)
|
||||
first().maybeParseBySeparators(settings)
|
||||
} else {
|
||||
// Nothing to do.
|
||||
this
|
||||
|
@ -82,7 +83,7 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): List<String> {
|
|||
|
||||
/**
|
||||
* Fix trailing whitespace or blank contents in a [String].
|
||||
* @return A string with trailing whitespace removed or null if the [String] was all whitespace or
|
||||
* @return A string with trailing whitespace remove,d or null if the [String] was all whitespace or
|
||||
* empty.
|
||||
*/
|
||||
fun String.correctWhitespace() = trim().ifBlank { null }
|
||||
|
@ -95,14 +96,12 @@ fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() }
|
|||
|
||||
/**
|
||||
* Attempt to parse a string by the user's separator preferences.
|
||||
* @param separators A string of characters to split by. Can be empty.
|
||||
* @return A list of one or more [String]s that were split up by the given separators.
|
||||
* @param settings [Settings] required to obtain user separator configuration.
|
||||
* @return A list of one or more [String]s that were split up by the user-defined separators.
|
||||
*/
|
||||
private fun String.maybeParseBySeparators(separators: String) =
|
||||
if (separators.isNotEmpty()) {
|
||||
splitEscaped { separators.contains(it) }.correctWhitespace()
|
||||
} else {
|
||||
listOf(this)
|
||||
private fun String.maybeParseBySeparators(settings: MusicSettings): List<String> {
|
||||
// Get the separators the user desires. If null, there's nothing to do.
|
||||
return splitEscaped { settings.multiValueSeparators.contains(it) }.correctWhitespace()
|
||||
}
|
||||
|
||||
/// --- ID3v2 PARSING ---
|
||||
|
@ -119,20 +118,30 @@ fun String.parseId3v2Position() = split('/', limit = 2)[0].toIntOrNull()?.nonZer
|
|||
* Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer
|
||||
* representations of genre fields into their named counterparts, and split up singular ID3v2-style
|
||||
* integer genre fields into one or more genres.
|
||||
* @param separators A string of characters to split by. Can be empty.
|
||||
* @return A list of one or more genre names.
|
||||
* @param settings [MusicSettings] required to obtain user separator configuration.
|
||||
* @return A list of one or more genre names..
|
||||
*/
|
||||
fun List<String>.parseId3GenreNames(separators: String) =
|
||||
fun List<String>.parseId3GenreNames(settings: MusicSettings) =
|
||||
if (size == 1) {
|
||||
first().parseId3MultiValueGenre(separators)
|
||||
first().parseId3MultiValueGenre(settings)
|
||||
} else {
|
||||
// Nothing to split, just map any ID3v1 genres to their name counterparts.
|
||||
map { it.parseId3v1Genre() ?: it }
|
||||
}
|
||||
|
||||
private fun String.parseId3MultiValueGenre(separators: String) =
|
||||
parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: maybeParseBySeparators(separators)
|
||||
/**
|
||||
* Parse a single ID3v1/ID3v2 integer genre field into their named representations.
|
||||
* @param settings [MusicSettings] required to obtain user separator configuration.
|
||||
* @return A list of one or more genre names.
|
||||
*/
|
||||
private fun String.parseId3MultiValueGenre(settings: MusicSettings) =
|
||||
parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: maybeParseBySeparators(settings)
|
||||
|
||||
/**
|
||||
* Parse an ID3v1 integer genre field.
|
||||
* @return A named genre if the field is a valid integer, "Cover" or "Remix" if the field is
|
||||
* "CR"/"RX" respectively, and nothing if the field is not a valid ID3v1 integer genre.
|
||||
*/
|
||||
private fun String.parseId3v1Genre(): String? {
|
||||
// ID3v1 genres are a plain integer value without formatting, so in that case
|
||||
// try to index the genre table with such.
|
||||
|
@ -155,6 +164,11 @@ private fun String.parseId3v1Genre(): String? {
|
|||
*/
|
||||
private val ID3V2_GENRE_RE = Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?")
|
||||
|
||||
/**
|
||||
* Parse an ID3v2 integer genre field, which has support for multiple genre values and combined
|
||||
* named/integer genres.
|
||||
* @return A list of one or more genres, or null if the field is not a valid ID3v2 integer genre.
|
||||
*/
|
||||
private fun String.parseId3v2Genre(): List<String>? {
|
||||
val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues
|
||||
val genres = mutableSetOf<String>()
|
||||
|
|
|
@ -25,7 +25,7 @@ import com.google.android.material.checkbox.MaterialCheckBox
|
|||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||
|
||||
/**
|
||||
|
@ -42,7 +42,7 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
|
|||
.setTitle(R.string.set_separators)
|
||||
.setNegativeButton(R.string.lbl_cancel, null)
|
||||
.setPositiveButton(R.string.lbl_save) { _, _ ->
|
||||
Settings(requireContext()).musicSeparators = getCurrentSeparators()
|
||||
MusicSettings.from(requireContext()).multiValueSeparators = getCurrentSeparators()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,8 +59,8 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
|
|||
// the corresponding CheckBox for each character instead of doing an iteration
|
||||
// through the separator list for each CheckBox.
|
||||
(savedInstanceState?.getString(KEY_PENDING_SEPARATORS)
|
||||
?: Settings(requireContext()).musicSeparators)
|
||||
?.forEach {
|
||||
?: MusicSettings.from(requireContext()).multiValueSeparators)
|
||||
.forEach {
|
||||
when (it) {
|
||||
Separators.COMMA -> binding.separatorComma.isChecked = true
|
||||
Separators.SEMICOLON -> binding.separatorSemicolon.isChecked = true
|
||||
|
|
|
@ -21,6 +21,8 @@ import androidx.lifecycle.ViewModel
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.Library
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
/**
|
||||
|
|
|
@ -30,7 +30,7 @@ import androidx.core.view.isVisible
|
|||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -56,14 +56,11 @@ class MusicDirsDialog :
|
|||
.setNeutralButton(R.string.lbl_add, null)
|
||||
.setNegativeButton(R.string.lbl_cancel, null)
|
||||
.setPositiveButton(R.string.lbl_save) { _, _ ->
|
||||
val settings = Settings(requireContext())
|
||||
val dirs =
|
||||
settings.getMusicDirs(
|
||||
requireNotNull(storageManager) { "StorageManager was not available" })
|
||||
val settings = MusicSettings.from(requireContext())
|
||||
val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
|
||||
if (dirs != newDirs) {
|
||||
if (settings.musicDirs != newDirs) {
|
||||
logD("Committing changes")
|
||||
settings.setMusicDirs(newDirs)
|
||||
settings.musicDirs = newDirs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -104,7 +101,7 @@ class MusicDirsDialog :
|
|||
itemAnimator = null
|
||||
}
|
||||
|
||||
var dirs = Settings(context).getMusicDirs(storageManager)
|
||||
var dirs = MusicSettings.from(context).musicDirs
|
||||
if (savedInstanceState != null) {
|
||||
val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS)
|
||||
if (pendingDirs != null) {
|
||||
|
|
|
@ -28,8 +28,8 @@ import kotlinx.coroutines.withContext
|
|||
import kotlinx.coroutines.yield
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.Library
|
||||
import org.oxycblt.auxio.music.extractor.*
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
@ -224,7 +224,7 @@ class Indexer private constructor() {
|
|||
// Build the rest of the music library from the song list. This is much more powerful
|
||||
// and reliable compared to using MediaStore to obtain grouping information.
|
||||
val buildStart = System.currentTimeMillis()
|
||||
val library = Library(rawSongs, Settings(context))
|
||||
val library = Library(rawSongs, MusicSettings.from(context))
|
||||
logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
|
||||
return library
|
||||
}
|
||||
|
|
|
@ -33,11 +33,11 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.storage.contentResolverSafe
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.service.ForegroundManager
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -68,7 +68,7 @@ class IndexerService :
|
|||
private lateinit var observingNotification: ObservingNotification
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
private lateinit var indexerContentObserver: SystemContentObserver
|
||||
private lateinit var settings: Settings
|
||||
private lateinit var settings: MusicSettings
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
@ -83,7 +83,7 @@ class IndexerService :
|
|||
// Initialize any listener-dependent components last as we wouldn't want a listener race
|
||||
// condition to cause us to load music before we were fully initialize.
|
||||
indexerContentObserver = SystemContentObserver()
|
||||
settings = Settings(this)
|
||||
settings = MusicSettings.from(this)
|
||||
settings.addListener(this)
|
||||
indexer.registerController(this)
|
||||
// An indeterminate indexer and a missing library implies we are extremely early
|
||||
|
|
|
@ -24,7 +24,6 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
|
@ -66,7 +65,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
|||
|
||||
// Set up actions
|
||||
binding.playbackPlayPause.setOnClickListener { playbackModel.toggleIsPlaying() }
|
||||
setupSecondaryActions(binding, Settings(context))
|
||||
setupSecondaryActions(binding, PlaybackSettings.from(context).playbackBarAction)
|
||||
|
||||
// Load the track color in manually as it's unclear whether the track actually supports
|
||||
// using a ColorStateList in the resources.
|
||||
|
@ -86,8 +85,8 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
|||
binding.playbackInfo.isSelected = false
|
||||
}
|
||||
|
||||
private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, settings: Settings) {
|
||||
when (settings.playbackBarAction) {
|
||||
private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, actionMode: ActionMode) {
|
||||
when (actionMode) {
|
||||
ActionMode.NEXT -> {
|
||||
binding.playbackSecondaryAction.apply {
|
||||
setIconResource(R.drawable.ic_skip_next_24)
|
||||
|
|
213
app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt
Normal file
213
app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt
Normal file
|
@ -0,0 +1,213 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.playback
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* User configuration specific to the playback system.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface PlaybackSettings : Settings {
|
||||
/** The action to display on the playback bar. */
|
||||
val playbackBarAction: ActionMode
|
||||
/** The action to display in the playback notification. */
|
||||
val playbackNotificationAction: ActionMode
|
||||
/** Whether to start playback when a headset is plugged in. */
|
||||
val headsetAutoplay: Boolean
|
||||
/** The current ReplayGain configuration. */
|
||||
val replayGainMode: ReplayGainMode
|
||||
/** The current ReplayGain pre-amp configuration. */
|
||||
var replayGainPreAmp: ReplayGainPreAmp
|
||||
/**
|
||||
* What type of MusicParent to play from when a Song is played from a list of other items. Null
|
||||
* if to play from all Songs.
|
||||
*/
|
||||
val inListPlaybackMode: MusicMode
|
||||
/**
|
||||
* What type of MusicParent to play from when a Song is played from within an item (ex. like in
|
||||
* the detail view). Null if to play from the item it was played in.
|
||||
*/
|
||||
val inParentPlaybackMode: MusicMode?
|
||||
/** Whether to keep shuffle on when playing a new Song. */
|
||||
val keepShuffle: Boolean
|
||||
/** Whether to rewind when the skip previous button is pressed before skipping back. */
|
||||
val rewindWithPrev: Boolean
|
||||
/** Whether a song should pause after every repeat. */
|
||||
val pauseOnRepeat: Boolean
|
||||
|
||||
private class Real(context: Context) : Settings.Real(context), PlaybackSettings {
|
||||
override val inListPlaybackMode: MusicMode
|
||||
get() =
|
||||
MusicMode.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_library_song_playback_mode),
|
||||
Int.MIN_VALUE))
|
||||
?: MusicMode.SONGS
|
||||
|
||||
override val inParentPlaybackMode: MusicMode?
|
||||
get() =
|
||||
MusicMode.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_detail_song_playback_mode),
|
||||
Int.MIN_VALUE))
|
||||
|
||||
override val playbackBarAction: ActionMode
|
||||
get() =
|
||||
ActionMode.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_bar_action), Int.MIN_VALUE))
|
||||
?: ActionMode.NEXT
|
||||
|
||||
override val playbackNotificationAction: ActionMode
|
||||
get() =
|
||||
ActionMode.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_notif_action), Int.MIN_VALUE))
|
||||
?: ActionMode.REPEAT
|
||||
|
||||
override val headsetAutoplay: Boolean
|
||||
get() =
|
||||
sharedPreferences.getBoolean(
|
||||
context.getString(R.string.set_key_headset_autoplay), false)
|
||||
|
||||
override val replayGainMode: ReplayGainMode
|
||||
get() =
|
||||
ReplayGainMode.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE))
|
||||
?: ReplayGainMode.DYNAMIC
|
||||
|
||||
override var replayGainPreAmp: ReplayGainPreAmp
|
||||
get() =
|
||||
ReplayGainPreAmp(
|
||||
sharedPreferences.getFloat(
|
||||
context.getString(R.string.set_key_pre_amp_with), 0f),
|
||||
sharedPreferences.getFloat(
|
||||
context.getString(R.string.set_key_pre_amp_without), 0f))
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putFloat(context.getString(R.string.set_key_pre_amp_with), value.with)
|
||||
putFloat(context.getString(R.string.set_key_pre_amp_without), value.without)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override val keepShuffle: Boolean
|
||||
get() =
|
||||
sharedPreferences.getBoolean(context.getString(R.string.set_key_keep_shuffle), true)
|
||||
|
||||
override val rewindWithPrev: Boolean
|
||||
get() =
|
||||
sharedPreferences.getBoolean(context.getString(R.string.set_key_rewind_prev), true)
|
||||
|
||||
override val pauseOnRepeat: Boolean
|
||||
get() =
|
||||
sharedPreferences.getBoolean(
|
||||
context.getString(R.string.set_key_repeat_pause), false)
|
||||
|
||||
override fun migrate() {
|
||||
// "Use alternate notification action" was converted to an ActionMode setting in 3.0.0.
|
||||
if (sharedPreferences.contains(OLD_KEY_ALT_NOTIF_ACTION)) {
|
||||
logD("Migrating $OLD_KEY_ALT_NOTIF_ACTION")
|
||||
|
||||
val mode =
|
||||
if (sharedPreferences.getBoolean(OLD_KEY_ALT_NOTIF_ACTION, false)) {
|
||||
ActionMode.SHUFFLE
|
||||
} else {
|
||||
ActionMode.REPEAT
|
||||
}
|
||||
|
||||
sharedPreferences.edit {
|
||||
putInt(context.getString(R.string.set_key_notif_action), mode.intCode)
|
||||
remove(OLD_KEY_ALT_NOTIF_ACTION)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
// PlaybackMode was converted to MusicMode in 3.0.0
|
||||
|
||||
fun Int.migratePlaybackMode() =
|
||||
when (this) {
|
||||
// Convert PlaybackMode into MusicMode
|
||||
IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS
|
||||
IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS
|
||||
IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS
|
||||
IntegerTable.PLAYBACK_MODE_IN_GENRE -> MusicMode.GENRES
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (sharedPreferences.contains(OLD_KEY_LIB_PLAYBACK_MODE)) {
|
||||
logD("Migrating $OLD_KEY_LIB_PLAYBACK_MODE")
|
||||
|
||||
val mode =
|
||||
sharedPreferences
|
||||
.getInt(OLD_KEY_LIB_PLAYBACK_MODE, IntegerTable.PLAYBACK_MODE_ALL_SONGS)
|
||||
.migratePlaybackMode()
|
||||
?: MusicMode.SONGS
|
||||
|
||||
sharedPreferences.edit {
|
||||
putInt(
|
||||
context.getString(R.string.set_key_library_song_playback_mode),
|
||||
mode.intCode)
|
||||
remove(OLD_KEY_LIB_PLAYBACK_MODE)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
if (sharedPreferences.contains(OLD_KEY_DETAIL_PLAYBACK_MODE)) {
|
||||
logD("Migrating $OLD_KEY_DETAIL_PLAYBACK_MODE")
|
||||
|
||||
val mode =
|
||||
sharedPreferences
|
||||
.getInt(OLD_KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE)
|
||||
.migratePlaybackMode()
|
||||
|
||||
sharedPreferences.edit {
|
||||
putInt(
|
||||
context.getString(R.string.set_key_detail_song_playback_mode),
|
||||
mode?.intCode ?: Int.MIN_VALUE)
|
||||
remove(OLD_KEY_DETAIL_PLAYBACK_MODE)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val OLD_KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION"
|
||||
const val OLD_KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2"
|
||||
const val OLD_KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Get a framework-backed implementation.
|
||||
* @param context [Context] required.
|
||||
*/
|
||||
fun from(context: Context): PlaybackSettings = Real(context)
|
||||
}
|
||||
}
|
|
@ -25,9 +25,9 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.home.HomeSettings
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.playback.state.*
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.context
|
||||
|
||||
/**
|
||||
|
@ -36,7 +36,9 @@ import org.oxycblt.auxio.util.context
|
|||
*/
|
||||
class PlaybackViewModel(application: Application) :
|
||||
AndroidViewModel(application), PlaybackStateManager.Listener {
|
||||
private val settings = Settings(application)
|
||||
private val homeSettings = HomeSettings.from(application)
|
||||
private val musicSettings = MusicSettings.from(application)
|
||||
private val playbackSettings = PlaybackSettings.from(application)
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private var lastPositionJob: Job? = null
|
||||
|
||||
|
@ -249,17 +251,17 @@ class PlaybackViewModel(application: Application) :
|
|||
private fun playImpl(
|
||||
song: Song?,
|
||||
parent: MusicParent?,
|
||||
shuffled: Boolean = playbackManager.queue.isShuffled && settings.keepShuffle
|
||||
shuffled: Boolean = playbackManager.queue.isShuffled && playbackSettings.keepShuffle
|
||||
) {
|
||||
check(song == null || parent == null || parent.songs.contains(song)) {
|
||||
"Song to play not in parent"
|
||||
}
|
||||
val sort =
|
||||
when (parent) {
|
||||
is Genre -> settings.detailGenreSort
|
||||
is Artist -> settings.detailArtistSort
|
||||
is Album -> settings.detailAlbumSort
|
||||
null -> settings.libSongSort
|
||||
is Genre -> musicSettings.genreSongSort
|
||||
is Artist -> musicSettings.artistSongSort
|
||||
is Album -> musicSettings.albumSongSort
|
||||
null -> musicSettings.songSort
|
||||
}
|
||||
playbackManager.play(song, parent, sort, shuffled)
|
||||
}
|
||||
|
@ -301,7 +303,7 @@ class PlaybackViewModel(application: Application) :
|
|||
* @param album The [Album] to add.
|
||||
*/
|
||||
fun playNext(album: Album) {
|
||||
playbackManager.playNext(settings.detailAlbumSort.songs(album.songs))
|
||||
playbackManager.playNext(musicSettings.albumSongSort.songs(album.songs))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -309,7 +311,7 @@ class PlaybackViewModel(application: Application) :
|
|||
* @param artist The [Artist] to add.
|
||||
*/
|
||||
fun playNext(artist: Artist) {
|
||||
playbackManager.playNext(settings.detailArtistSort.songs(artist.songs))
|
||||
playbackManager.playNext(musicSettings.artistSongSort.songs(artist.songs))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -317,7 +319,7 @@ class PlaybackViewModel(application: Application) :
|
|||
* @param genre The [Genre] to add.
|
||||
*/
|
||||
fun playNext(genre: Genre) {
|
||||
playbackManager.playNext(settings.detailGenreSort.songs(genre.songs))
|
||||
playbackManager.playNext(musicSettings.genreSongSort.songs(genre.songs))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -341,7 +343,7 @@ class PlaybackViewModel(application: Application) :
|
|||
* @param album The [Album] to add.
|
||||
*/
|
||||
fun addToQueue(album: Album) {
|
||||
playbackManager.addToQueue(settings.detailAlbumSort.songs(album.songs))
|
||||
playbackManager.addToQueue(musicSettings.albumSongSort.songs(album.songs))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -349,7 +351,7 @@ class PlaybackViewModel(application: Application) :
|
|||
* @param artist The [Artist] to add.
|
||||
*/
|
||||
fun addToQueue(artist: Artist) {
|
||||
playbackManager.addToQueue(settings.detailArtistSort.songs(artist.songs))
|
||||
playbackManager.addToQueue(musicSettings.artistSongSort.songs(artist.songs))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -357,7 +359,7 @@ class PlaybackViewModel(application: Application) :
|
|||
* @param genre The [Genre] to add.
|
||||
*/
|
||||
fun addToQueue(genre: Genre) {
|
||||
playbackManager.addToQueue(settings.detailGenreSort.songs(genre.songs))
|
||||
playbackManager.addToQueue(musicSettings.genreSongSort.songs(genre.songs))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -434,9 +436,9 @@ class PlaybackViewModel(application: Application) :
|
|||
private fun selectionToSongs(selection: List<Music>): List<Song> {
|
||||
return selection.flatMap {
|
||||
when (it) {
|
||||
is Album -> settings.detailAlbumSort.songs(it.songs)
|
||||
is Artist -> settings.detailArtistSort.songs(it.songs)
|
||||
is Genre -> settings.detailGenreSort.songs(it.songs)
|
||||
is Album -> musicSettings.albumSongSort.songs(it.songs)
|
||||
is Artist -> musicSettings.artistSongSort.songs(it.songs)
|
||||
is Genre -> musicSettings.genreSongSort.songs(it.songs)
|
||||
is Song -> listOf(it)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import androidx.appcompat.app.AlertDialog
|
|||
import kotlin.math.abs
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogPreAmpBinding
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||
|
||||
/**
|
||||
|
@ -39,11 +39,11 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
|
|||
.setTitle(R.string.set_pre_amp)
|
||||
.setPositiveButton(R.string.lbl_ok) { _, _ ->
|
||||
val binding = requireBinding()
|
||||
Settings(requireContext()).replayGainPreAmp =
|
||||
PlaybackSettings.from(requireContext()).replayGainPreAmp =
|
||||
ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value)
|
||||
}
|
||||
.setNeutralButton(R.string.lbl_reset) { _, _ ->
|
||||
Settings(requireContext()).replayGainPreAmp = ReplayGainPreAmp(0f, 0f)
|
||||
PlaybackSettings.from(requireContext()).replayGainPreAmp = ReplayGainPreAmp(0f, 0f)
|
||||
}
|
||||
.setNegativeButton(R.string.lbl_cancel, null)
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
|
|||
// First initialization, we need to supply the sliders with the values from
|
||||
// settings. After this, the sliders save their own state, so we do not need to
|
||||
// do any restore behavior.
|
||||
val preAmp = Settings(requireContext()).replayGainPreAmp
|
||||
val preAmp = PlaybackSettings.from(requireContext()).replayGainPreAmp
|
||||
binding.withTagsSlider.value = preAmp.with
|
||||
binding.withoutTagsSlider.value = preAmp.without
|
||||
}
|
||||
|
|
|
@ -31,8 +31,8 @@ import kotlin.math.pow
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.extractor.TextTags
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
|
@ -48,7 +48,7 @@ import org.oxycblt.auxio.util.logD
|
|||
class ReplayGainAudioProcessor(private val context: Context) :
|
||||
BaseAudioProcessor(), Player.Listener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val settings = Settings(context)
|
||||
private val settings = PlaybackSettings.from(context)
|
||||
private var lastFormat: Format? = null
|
||||
|
||||
private var volume = 1f
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.database.sqlite.SQLiteOpenHelper
|
|||
import android.provider.BaseColumns
|
||||
import androidx.core.database.sqlite.transaction
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.Library
|
||||
import org.oxycblt.auxio.util.*
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,6 +21,9 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.Library
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
|
|
|
@ -34,11 +34,11 @@ import org.oxycblt.auxio.image.BitmapProvider
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.ActionMode
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.playback.state.Queue
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
|
@ -59,7 +59,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
|
|||
}
|
||||
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val settings = Settings(context)
|
||||
private val settings = PlaybackSettings.from(context)
|
||||
|
||||
private val notification = NotificationComponent(context, mediaSession.sessionToken)
|
||||
private val provider = BitmapProvider(context)
|
||||
|
|
|
@ -44,15 +44,16 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.music.Library
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.service.ForegroundManager
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.widgets.WidgetComponent
|
||||
import org.oxycblt.auxio.widgets.WidgetProvider
|
||||
|
@ -92,7 +93,8 @@ class PlaybackService :
|
|||
// Managers
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
private lateinit var settings: Settings
|
||||
private lateinit var musicSettings: MusicSettings
|
||||
private lateinit var playbackSettings: PlaybackSettings
|
||||
|
||||
// State
|
||||
private lateinit var foregroundManager: ForegroundManager
|
||||
|
@ -143,7 +145,8 @@ class PlaybackService :
|
|||
.also { it.addListener(this) }
|
||||
replayGainProcessor.addToListeners(player)
|
||||
// Initialize the core service components
|
||||
settings = Settings(this)
|
||||
musicSettings = MusicSettings.from(this)
|
||||
playbackSettings = PlaybackSettings.from(this)
|
||||
foregroundManager = ForegroundManager(this)
|
||||
// Initialize any listener-dependent components last as we wouldn't want a listener race
|
||||
// condition to cause us to load music before we were fully initialize.
|
||||
|
@ -213,7 +216,7 @@ class PlaybackService :
|
|||
get() = player.audioSessionId
|
||||
|
||||
override val shouldRewindWithPrev: Boolean
|
||||
get() = settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
|
||||
get() = playbackSettings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
|
||||
|
||||
override fun getState(durationMs: Long) =
|
||||
InternalPlayer.State.from(
|
||||
|
@ -286,7 +289,7 @@ class PlaybackService :
|
|||
if (playbackManager.repeatMode == RepeatMode.TRACK) {
|
||||
playbackManager.rewind()
|
||||
// May be configured to pause when we repeat a track.
|
||||
if (settings.pauseOnRepeat) {
|
||||
if (playbackSettings.pauseOnRepeat) {
|
||||
playbackManager.setPlaying(false)
|
||||
}
|
||||
} else {
|
||||
|
@ -352,7 +355,7 @@ class PlaybackService :
|
|||
}
|
||||
// Shuffle all -> Start new playback from all songs
|
||||
is InternalPlayer.Action.ShuffleAll -> {
|
||||
playbackManager.play(null, null, settings.libSongSort, true)
|
||||
playbackManager.play(null, null, musicSettings.songSort, true)
|
||||
}
|
||||
// Open -> Try to find the Song for the given file and then play it from all songs
|
||||
is InternalPlayer.Action.Open -> {
|
||||
|
@ -360,8 +363,8 @@ class PlaybackService :
|
|||
playbackManager.play(
|
||||
song,
|
||||
null,
|
||||
settings.libSongSort,
|
||||
playbackManager.queue.isShuffled && settings.keepShuffle)
|
||||
musicSettings.songSort,
|
||||
playbackManager.queue.isShuffled && playbackSettings.keepShuffle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -431,7 +434,7 @@ class PlaybackService :
|
|||
// ACTION_HEADSET_PLUG will fire when this BroadcastReciever is initially attached,
|
||||
// which would result in unexpected playback. Work around it by dropping the first
|
||||
// call to this function, which should come from that Intent.
|
||||
if (settings.headsetAutoplay &&
|
||||
if (playbackSettings.headsetAutoplay &&
|
||||
playbackManager.queue.currentSong != null &&
|
||||
initialHeadsetPlugEventHandled) {
|
||||
logD("Device connected, resuming")
|
||||
|
|
|
@ -38,7 +38,7 @@ import org.oxycblt.auxio.music.Music
|
|||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.util.*
|
||||
|
||||
/**
|
||||
|
@ -137,7 +137,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
|||
override fun onRealClick(item: Music) {
|
||||
when (item) {
|
||||
is Song ->
|
||||
when (Settings(requireContext()).libPlaybackMode) {
|
||||
when (PlaybackSettings.from(requireContext()).inListPlaybackMode) {
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||
|
|
57
app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt
Normal file
57
app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.search
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
|
||||
/**
|
||||
* User configuration specific to the search UI.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface SearchSettings : Settings {
|
||||
/** The type of Music the search view is currently filtering to. */
|
||||
var searchFilterMode: MusicMode?
|
||||
|
||||
private class Real(context: Context) : Settings.Real(context), SearchSettings {
|
||||
override var searchFilterMode: MusicMode?
|
||||
get() =
|
||||
MusicMode.fromIntCode(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_search_filter), Int.MIN_VALUE))
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putInt(
|
||||
context.getString(R.string.set_key_search_filter),
|
||||
value?.intCode ?: Int.MIN_VALUE)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Get a framework-backed implementation.
|
||||
* @param context [Context] required.
|
||||
*/
|
||||
fun from(context: Context): SearchSettings = Real(context)
|
||||
}
|
||||
}
|
|
@ -31,7 +31,9 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.list.Header
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.music.Library
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -42,7 +44,7 @@ import org.oxycblt.auxio.util.logD
|
|||
class SearchViewModel(application: Application) :
|
||||
AndroidViewModel(application), MusicStore.Listener {
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
private val settings = Settings(context)
|
||||
private val settings = SearchSettings.from(application)
|
||||
private var lastQuery: String? = null
|
||||
private var currentSearchJob: Job? = null
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -19,445 +19,49 @@ package org.oxycblt.auxio.settings
|
|||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Build
|
||||
import android.os.storage.StorageManager
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.home.tabs.Tab
|
||||
import org.oxycblt.auxio.image.CoverMode
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.music.storage.Directory
|
||||
import org.oxycblt.auxio.music.storage.MusicDirectories
|
||||
import org.oxycblt.auxio.playback.ActionMode
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
|
||||
import org.oxycblt.auxio.ui.accent.Accent
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
/**
|
||||
* A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. Member
|
||||
* mutability is dependent on how they are used in app. Immutable members are often only modified by
|
||||
* the preferences view, while mutable members are modified elsewhere.
|
||||
* Abstract user configuration information. This interface has no functionality whatsoever. Concrete
|
||||
* implementations should be preferred instead.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class Settings(private val context: Context) {
|
||||
private val inner = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
|
||||
|
||||
/**
|
||||
* Migrate any settings from an old version into their modern counterparts. This can cause data
|
||||
* loss depending on the feasibility of a migration.
|
||||
*/
|
||||
interface Settings {
|
||||
/** Migrate any settings fields from older versions into their new counterparts. */
|
||||
fun migrate() {
|
||||
if (inner.contains(OldKeys.KEY_ACCENT3)) {
|
||||
logD("Migrating ${OldKeys.KEY_ACCENT3}")
|
||||
|
||||
var accent = inner.getInt(OldKeys.KEY_ACCENT3, 5)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// Accents were previously frozen as soon as the OS was updated to android twelve,
|
||||
// as dynamic colors were enabled by default. This is no longer the case, so we need
|
||||
// to re-update the setting to dynamic colors here.
|
||||
accent = 16
|
||||
}
|
||||
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_accent), accent)
|
||||
remove(OldKeys.KEY_ACCENT3)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
if (inner.contains(OldKeys.KEY_SHOW_COVERS) || inner.contains(OldKeys.KEY_QUALITY_COVERS)) {
|
||||
logD("Migrating cover settings")
|
||||
|
||||
val mode =
|
||||
when {
|
||||
!inner.getBoolean(OldKeys.KEY_SHOW_COVERS, true) -> CoverMode.OFF
|
||||
!inner.getBoolean(OldKeys.KEY_QUALITY_COVERS, true) -> CoverMode.MEDIA_STORE
|
||||
else -> CoverMode.QUALITY
|
||||
}
|
||||
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_cover_mode), mode.intCode)
|
||||
remove(OldKeys.KEY_SHOW_COVERS)
|
||||
remove(OldKeys.KEY_QUALITY_COVERS)
|
||||
}
|
||||
}
|
||||
|
||||
if (inner.contains(OldKeys.KEY_ALT_NOTIF_ACTION)) {
|
||||
logD("Migrating ${OldKeys.KEY_ALT_NOTIF_ACTION}")
|
||||
|
||||
val mode =
|
||||
if (inner.getBoolean(OldKeys.KEY_ALT_NOTIF_ACTION, false)) {
|
||||
ActionMode.SHUFFLE
|
||||
} else {
|
||||
ActionMode.REPEAT
|
||||
}
|
||||
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_notif_action), mode.intCode)
|
||||
remove(OldKeys.KEY_ALT_NOTIF_ACTION)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun Int.migratePlaybackMode() =
|
||||
when (this) {
|
||||
// Convert PlaybackMode into MusicMode
|
||||
IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS
|
||||
IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS
|
||||
IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS
|
||||
IntegerTable.PLAYBACK_MODE_IN_GENRE -> MusicMode.GENRES
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (inner.contains(OldKeys.KEY_LIB_PLAYBACK_MODE)) {
|
||||
logD("Migrating ${OldKeys.KEY_LIB_PLAYBACK_MODE}")
|
||||
|
||||
val mode =
|
||||
inner
|
||||
.getInt(OldKeys.KEY_LIB_PLAYBACK_MODE, IntegerTable.PLAYBACK_MODE_ALL_SONGS)
|
||||
.migratePlaybackMode()
|
||||
?: MusicMode.SONGS
|
||||
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_library_song_playback_mode), mode.intCode)
|
||||
remove(OldKeys.KEY_LIB_PLAYBACK_MODE)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
if (inner.contains(OldKeys.KEY_DETAIL_PLAYBACK_MODE)) {
|
||||
logD("Migrating ${OldKeys.KEY_DETAIL_PLAYBACK_MODE}")
|
||||
|
||||
val mode =
|
||||
inner.getInt(OldKeys.KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE).migratePlaybackMode()
|
||||
|
||||
inner.edit {
|
||||
putInt(
|
||||
context.getString(R.string.set_key_detail_song_playback_mode),
|
||||
mode?.intCode ?: Int.MIN_VALUE)
|
||||
remove(OldKeys.KEY_DETAIL_PLAYBACK_MODE)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a [SharedPreferences.OnSharedPreferenceChangeListener] to monitor for settings updates.
|
||||
* @param listener The [SharedPreferences.OnSharedPreferenceChangeListener] to add.
|
||||
*/
|
||||
fun addListener(listener: OnSharedPreferenceChangeListener) {
|
||||
inner.registerOnSharedPreferenceChangeListener(listener)
|
||||
fun addListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a [SharedPreferences.OnSharedPreferenceChangeListener], preventing any further
|
||||
* settings updates from being sent to ti.t
|
||||
*/
|
||||
fun removeListener(listener: OnSharedPreferenceChangeListener) {
|
||||
inner.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
|
||||
// --- VALUES ---
|
||||
|
||||
/** The current theme. Represented by the [AppCompatDelegate] constants. */
|
||||
val theme: Int
|
||||
get() =
|
||||
inner.getInt(
|
||||
context.getString(R.string.set_key_theme),
|
||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
|
||||
/** Whether to use a black background when a dark theme is currently used. */
|
||||
val useBlackTheme: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_black_theme), false)
|
||||
|
||||
/** The current [Accent] (Color Scheme). */
|
||||
var accent: Accent
|
||||
get() =
|
||||
Accent.from(inner.getInt(context.getString(R.string.set_key_accent), Accent.DEFAULT))
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_accent), value.index)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** The tabs to show in the home UI. */
|
||||
var libTabs: Array<Tab>
|
||||
get() =
|
||||
Tab.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_lib_tabs), Tab.SEQUENCE_DEFAULT))
|
||||
?: unlikelyToBeNull(Tab.fromIntCode(Tab.SEQUENCE_DEFAULT))
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_tabs), Tab.toIntCode(value))
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether to hide artists considered "collaborators" from the home UI. */
|
||||
val shouldHideCollaborators: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_hide_collaborators), false)
|
||||
|
||||
/** Whether to round additional UI elements that require album covers to be rounded. */
|
||||
val roundMode: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_round_mode), false)
|
||||
|
||||
/** The action to display on the playback bar. */
|
||||
val playbackBarAction: ActionMode
|
||||
get() =
|
||||
ActionMode.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_bar_action), Int.MIN_VALUE))
|
||||
?: ActionMode.NEXT
|
||||
|
||||
/** The action to display in the playback notification. */
|
||||
val playbackNotificationAction: ActionMode
|
||||
get() =
|
||||
ActionMode.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_notif_action), Int.MIN_VALUE))
|
||||
?: ActionMode.REPEAT
|
||||
|
||||
/** Whether to start playback when a headset is plugged in. */
|
||||
val headsetAutoplay: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_headset_autoplay), false)
|
||||
|
||||
/** The current ReplayGain configuration. */
|
||||
val replayGainMode: ReplayGainMode
|
||||
get() =
|
||||
ReplayGainMode.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE))
|
||||
?: ReplayGainMode.DYNAMIC
|
||||
|
||||
/** The current ReplayGain pre-amp configuration. */
|
||||
var replayGainPreAmp: ReplayGainPreAmp
|
||||
get() =
|
||||
ReplayGainPreAmp(
|
||||
inner.getFloat(context.getString(R.string.set_key_pre_amp_with), 0f),
|
||||
inner.getFloat(context.getString(R.string.set_key_pre_amp_without), 0f))
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putFloat(context.getString(R.string.set_key_pre_amp_with), value.with)
|
||||
putFloat(context.getString(R.string.set_key_pre_amp_without), value.without)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** What MusicParent item to play from when a Song is played from the home view. */
|
||||
val libPlaybackMode: MusicMode
|
||||
get() =
|
||||
MusicMode.fromIntCode(
|
||||
inner.getInt(
|
||||
context.getString(R.string.set_key_library_song_playback_mode), Int.MIN_VALUE))
|
||||
?: MusicMode.SONGS
|
||||
|
||||
/**
|
||||
* What MusicParent item to play from when a Song is played from the detail view. Will be null
|
||||
* if configured to play from the currently shown item.
|
||||
*/
|
||||
val detailPlaybackMode: MusicMode?
|
||||
get() =
|
||||
MusicMode.fromIntCode(
|
||||
inner.getInt(
|
||||
context.getString(R.string.set_key_detail_song_playback_mode), Int.MIN_VALUE))
|
||||
|
||||
/** Whether to keep shuffle on when playing a new Song. */
|
||||
val keepShuffle: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_keep_shuffle), true)
|
||||
|
||||
/** Whether to rewind when the skip previous button is pressed before skipping back. */
|
||||
val rewindWithPrev: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_rewind_prev), true)
|
||||
|
||||
/** Whether a song should pause after every repeat. */
|
||||
val pauseOnRepeat: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false)
|
||||
|
||||
/** Whether to be actively watching for changes in the music library. */
|
||||
val shouldBeObserving: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_observing), false)
|
||||
|
||||
/** The strategy used when loading album covers. */
|
||||
val coverMode: CoverMode
|
||||
get() =
|
||||
CoverMode.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
|
||||
?: CoverMode.MEDIA_STORE
|
||||
|
||||
/** Whether to exclude non-music audio files from the music library. */
|
||||
val excludeNonMusic: Boolean
|
||||
get() = inner.getBoolean(context.getString(R.string.set_key_exclude_non_music), true)
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
val dirs =
|
||||
(inner.getStringSet(context.getString(R.string.set_key_music_dirs), null) ?: emptySet())
|
||||
.mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) }
|
||||
return MusicDirectories(
|
||||
dirs, inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false))
|
||||
fun removeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the configuration on how to handle particular directories in the music library.
|
||||
* @param musicDirs The new [MusicDirectories] configuration.
|
||||
* A framework-backed [Settings] implementation.
|
||||
* @param context [Context] required.
|
||||
*/
|
||||
fun setMusicDirs(musicDirs: MusicDirectories) {
|
||||
inner.edit {
|
||||
putStringSet(
|
||||
context.getString(R.string.set_key_music_dirs),
|
||||
musicDirs.dirs.map(Directory::toDocumentTreeUri).toSet())
|
||||
putBoolean(
|
||||
context.getString(R.string.set_key_music_dirs_include), musicDirs.shouldInclude)
|
||||
apply()
|
||||
}
|
||||
abstract class Real(protected val context: Context) : Settings {
|
||||
protected val sharedPreferences: SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
|
||||
|
||||
override fun addListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
// code. This makes it easier to use in Regexes and makes it more extendable.
|
||||
get() = inner.getString(context.getString(R.string.set_key_separators), "") ?: ""
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putString(context.getString(R.string.set_key_separators), value)
|
||||
apply()
|
||||
override fun removeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
/** The type of Music the search view is currently filtering to. */
|
||||
var searchFilterMode: MusicMode?
|
||||
get() =
|
||||
MusicMode.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_search_filter), Int.MIN_VALUE))
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(
|
||||
context.getString(R.string.set_key_search_filter),
|
||||
value?.intCode ?: Int.MIN_VALUE)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** The Song [Sort] mode used in the Home UI. */
|
||||
var libSongSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_songs_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** The Album [Sort] mode used in the Home UI. */
|
||||
var libAlbumSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_albums_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** The Artist [Sort] mode used in the Home UI. */
|
||||
var libArtistSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** The Genre [Sort] mode used in the Home UI. */
|
||||
var libGenreSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_genres_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** The [Sort] mode used in the Album Detail UI. */
|
||||
var detailAlbumSort: Sort
|
||||
get() {
|
||||
var sort =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(
|
||||
context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByDisc, true)
|
||||
|
||||
// Correct legacy album sort modes to Disc
|
||||
if (sort.mode is Sort.Mode.ByName) {
|
||||
sort = sort.withMode(Sort.Mode.ByDisc)
|
||||
}
|
||||
|
||||
return sort
|
||||
}
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_detail_album_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** The [Sort] mode used in the Artist Detail UI. */
|
||||
var detailArtistSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByDate, false)
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_detail_artist_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** The [Sort] mode used in the Genre Detail UI. */
|
||||
var detailGenreSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_detail_genre_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** Legacy keys that are no longer used, but still have to be migrated. */
|
||||
private object OldKeys {
|
||||
const val KEY_ACCENT3 = "auxio_accent"
|
||||
const val KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION"
|
||||
const val KEY_SHOW_COVERS = "KEY_SHOW_COVERS"
|
||||
const val KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS"
|
||||
const val KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2"
|
||||
const val KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@ import coil.Coil
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.settings.SettingsFragmentDirections
|
||||
import org.oxycblt.auxio.ui.UISettings
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.isNight
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -149,8 +149,6 @@ class PreferenceFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
private fun setupPreference(preference: Preference) {
|
||||
val settings = Settings(requireContext())
|
||||
|
||||
if (!preference.isVisible) {
|
||||
// Nothing to do.
|
||||
return
|
||||
|
@ -170,7 +168,7 @@ class PreferenceFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
}
|
||||
getString(R.string.set_key_accent) -> {
|
||||
preference.summary = getString(settings.accent.name)
|
||||
preference.summary = getString(UISettings.from(requireContext()).accent.name)
|
||||
}
|
||||
getString(R.string.set_key_black_theme) -> {
|
||||
preference.onPreferenceChangeListener =
|
||||
|
|
100
app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt
Normal file
100
app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.accent.Accent
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
interface UISettings : Settings {
|
||||
/** The current theme. Represented by the AppCompatDelegate constants. */
|
||||
val theme: Int
|
||||
/** Whether to use a black background when a dark theme is currently used. */
|
||||
val useBlackTheme: Boolean
|
||||
/** The current [Accent] (Color Scheme). */
|
||||
var accent: Accent
|
||||
/** Whether to round additional UI elements that require album covers to be rounded. */
|
||||
val roundMode: Boolean
|
||||
|
||||
private class Real(context: Context) : Settings.Real(context), UISettings {
|
||||
override val theme: Int
|
||||
get() =
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_theme),
|
||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
|
||||
override val useBlackTheme: Boolean
|
||||
get() =
|
||||
sharedPreferences.getBoolean(context.getString(R.string.set_key_black_theme), false)
|
||||
|
||||
override var accent: Accent
|
||||
get() =
|
||||
Accent.from(
|
||||
sharedPreferences.getInt(
|
||||
context.getString(R.string.set_key_accent), Accent.DEFAULT))
|
||||
set(value) {
|
||||
sharedPreferences.edit {
|
||||
putInt(context.getString(R.string.set_key_accent), value.index)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override val roundMode: Boolean
|
||||
get() =
|
||||
sharedPreferences.getBoolean(context.getString(R.string.set_key_round_mode), false)
|
||||
|
||||
override fun migrate() {
|
||||
if (sharedPreferences.contains(OLD_KEY_ACCENT3)) {
|
||||
logD("Migrating $OLD_KEY_ACCENT3")
|
||||
|
||||
var accent = sharedPreferences.getInt(OLD_KEY_ACCENT3, 5)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// Accents were previously frozen as soon as the OS was updated to android
|
||||
// twelve,
|
||||
// as dynamic colors were enabled by default. This is no longer the case, so we
|
||||
// need
|
||||
// to re-update the setting to dynamic colors here.
|
||||
accent = 16
|
||||
}
|
||||
|
||||
sharedPreferences.edit {
|
||||
putInt(context.getString(R.string.set_key_accent), accent)
|
||||
remove(OLD_KEY_ACCENT3)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val OLD_KEY_ACCENT3 = "auxio_accent"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Get a framework-backed implementation.
|
||||
* @param context [Context] required.
|
||||
*/
|
||||
fun from(context: Context): UISettings = Real(context)
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ import org.oxycblt.auxio.BuildConfig
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogAccentBinding
|
||||
import org.oxycblt.auxio.list.ClickableListListener
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.UISettings
|
||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
@ -44,7 +44,7 @@ class AccentCustomizeDialog :
|
|||
builder
|
||||
.setTitle(R.string.set_accent)
|
||||
.setPositiveButton(R.string.lbl_ok) { _, _ ->
|
||||
val settings = Settings(requireContext())
|
||||
val settings = UISettings.from(requireContext())
|
||||
if (accentAdapter.selectedAccent == settings.accent) {
|
||||
// Nothing to do.
|
||||
return@setPositiveButton
|
||||
|
@ -65,7 +65,7 @@ class AccentCustomizeDialog :
|
|||
if (savedInstanceState != null) {
|
||||
Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT))
|
||||
} else {
|
||||
Settings(requireContext()).accent
|
||||
UISettings.from(requireContext()).accent
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ import org.oxycblt.auxio.playback.state.InternalPlayer
|
|||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.playback.state.Queue
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.UISettings
|
||||
import org.oxycblt.auxio.util.getDimenPixels
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.logD
|
|||
class WidgetComponent(private val context: Context) :
|
||||
PlaybackStateManager.Listener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val settings = Settings(context)
|
||||
private val settings = UISettings.from(context)
|
||||
private val widgetProvider = WidgetProvider()
|
||||
private val provider = BitmapProvider(context)
|
||||
|
||||
|
|
|
@ -31,7 +31,6 @@ import org.oxycblt.auxio.BuildConfig
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.playback.system.PlaybackService
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.*
|
||||
|
||||
/**
|
||||
|
@ -197,7 +196,7 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
// Below API 31, enable a rounded bar only if round mode is enabled.
|
||||
// On API 31+, the bar should always be round in order to fit in with other widgets.
|
||||
val background =
|
||||
if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
if (useRoundedRemoteViews(context)) {
|
||||
R.drawable.ui_widget_bar_round
|
||||
} else {
|
||||
R.drawable.ui_widget_bar_system
|
||||
|
@ -216,7 +215,7 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
// On API 31+, the background should always be round in order to fit in with other
|
||||
// widgets.
|
||||
val background =
|
||||
if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
if (useRoundedRemoteViews(context)) {
|
||||
R.drawable.ui_widget_bg_round
|
||||
} else {
|
||||
R.drawable.ui_widget_bg_system
|
||||
|
|
|
@ -27,6 +27,7 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import kotlin.math.sqrt
|
||||
import org.oxycblt.auxio.ui.UISettings
|
||||
import org.oxycblt.auxio.util.isLandscape
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||
|
@ -132,3 +133,12 @@ fun AppWidgetManager.updateAppWidgetCompat(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether rounded UI elements are appropriate for the widget, either based on the current
|
||||
* settings or if the widget has to fit in aesthetically with other widgets.
|
||||
* @param context [Context] configuration to use.
|
||||
* @return true if to use round mode, false otherwise.
|
||||
*/
|
||||
fun useRoundedRemoteViews(context: Context) =
|
||||
UISettings.from(context).roundMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
|
|
|
@ -126,8 +126,8 @@ class DateTest {
|
|||
|
||||
@Test
|
||||
fun date_fromYearDate() {
|
||||
assertEquals("2016", Date.from(2016).toString())
|
||||
assertEquals("2016", Date.from("2016").toString())
|
||||
assertEquals("2016-08-16", Date.from(20160816).toString())
|
||||
assertEquals("2016-08-16", Date.from("20160816").toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music
|
||||
|
||||
import org.oxycblt.auxio.music.storage.MusicDirectories
|
||||
|
||||
interface FakeMusicSettings : MusicSettings {
|
||||
override var musicDirs: MusicDirectories
|
||||
get() = throw NotImplementedError()
|
||||
set(_) = throw NotImplementedError()
|
||||
override val excludeNonMusic: Boolean
|
||||
get() = throw NotImplementedError()
|
||||
override val shouldBeObserving: Boolean
|
||||
get() = throw NotImplementedError()
|
||||
override var multiValueSeparators: String
|
||||
get() = throw NotImplementedError()
|
||||
set(_) = throw NotImplementedError()
|
||||
override var songSort: Sort
|
||||
get() = throw NotImplementedError()
|
||||
set(_) = throw NotImplementedError()
|
||||
override var albumSort: Sort
|
||||
get() = throw NotImplementedError()
|
||||
set(_) = throw NotImplementedError()
|
||||
override var artistSort: Sort
|
||||
get() = throw NotImplementedError()
|
||||
set(_) = throw NotImplementedError()
|
||||
override var genreSort: Sort
|
||||
get() = throw NotImplementedError()
|
||||
set(_) = throw NotImplementedError()
|
||||
override var albumSongSort: Sort
|
||||
get() = throw NotImplementedError()
|
||||
set(_) = throw NotImplementedError()
|
||||
override var artistSongSort: Sort
|
||||
get() = throw NotImplementedError()
|
||||
set(_) = throw NotImplementedError()
|
||||
override var genreSongSort: Sort
|
||||
get() = throw NotImplementedError()
|
||||
set(_) = throw NotImplementedError()
|
||||
}
|
|
@ -19,22 +19,27 @@ package org.oxycblt.auxio.music.parsing
|
|||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.oxycblt.auxio.music.FakeMusicSettings
|
||||
|
||||
class ParsingUtilTest {
|
||||
@Test
|
||||
fun parseMultiValue_single() {
|
||||
assertEquals(listOf("a", "b", "c"), listOf("a,b,c").parseMultiValue(","))
|
||||
assertEquals(
|
||||
listOf("a", "b", "c"), listOf("a,b,c").parseMultiValue(SeparatorMusicSettings(",")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseMultiValue_many() {
|
||||
assertEquals(listOf("a", "b", "c"), listOf("a", "b", "c").parseMultiValue(","))
|
||||
assertEquals(
|
||||
listOf("a", "b", "c"),
|
||||
listOf("a", "b", "c").parseMultiValue(SeparatorMusicSettings(",")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseMultiValue_several() {
|
||||
assertEquals(
|
||||
listOf("a", "b", "c", "d", "e", "f"), listOf("a,b;c/d+e&f").parseMultiValue(",;/+&"))
|
||||
listOf("a", "b", "c", "d", "e", "f"),
|
||||
listOf("a,b;c/d+e&f").parseMultiValue(SeparatorMusicSettings(",;/+&")))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -105,37 +110,45 @@ class ParsingUtilTest {
|
|||
fun parseId3v2Genre_multi() {
|
||||
assertEquals(
|
||||
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
||||
listOf("Post-Rock", "Shoegaze", "Glitch").parseId3GenreNames(","))
|
||||
listOf("Post-Rock", "Shoegaze", "Glitch")
|
||||
.parseId3GenreNames(SeparatorMusicSettings(",")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseId3v2Genre_multiId3v1() {
|
||||
assertEquals(
|
||||
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
||||
listOf("176", "178", "Glitch").parseId3GenreNames(","))
|
||||
listOf("176", "178", "Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseId3v2Genre_wackId3() {
|
||||
assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(","))
|
||||
assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(SeparatorMusicSettings(",")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseId3v2Genre_singleId3v23() {
|
||||
assertEquals(
|
||||
listOf("Post-Rock", "Shoegaze", "Remix", "Cover", "Glitch"),
|
||||
listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(","))
|
||||
listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseId3v2Genre_singleSeparated() {
|
||||
assertEquals(
|
||||
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
||||
listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(","))
|
||||
listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parsId3v2Genre_singleId3v1() {
|
||||
assertEquals(listOf("Post-Rock"), listOf("176").parseId3GenreNames(","))
|
||||
assertEquals(
|
||||
listOf("Post-Rock"), listOf("176").parseId3GenreNames(SeparatorMusicSettings(",")))
|
||||
}
|
||||
|
||||
class SeparatorMusicSettings(private val separators: String) : FakeMusicSettings {
|
||||
override var multiValueSeparators: String
|
||||
get() = separators
|
||||
set(_) = throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue