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
|
package org.oxycblt.auxio
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,5 +30,10 @@ import org.junit.runner.RunWith
|
||||||
*/
|
*/
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class StubTest {
|
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.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
import coil.request.CachePolicy
|
import coil.request.CachePolicy
|
||||||
|
import org.oxycblt.auxio.image.ImageSettings
|
||||||
import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher
|
import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher
|
||||||
import org.oxycblt.auxio.image.extractor.ArtistImageFetcher
|
import org.oxycblt.auxio.image.extractor.ArtistImageFetcher
|
||||||
import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFactory
|
import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFactory
|
||||||
import org.oxycblt.auxio.image.extractor.GenreImageFetcher
|
import org.oxycblt.auxio.image.extractor.GenreImageFetcher
|
||||||
import org.oxycblt.auxio.image.extractor.MusicKeyer
|
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.
|
* Auxio: A simple, rational music player for android.
|
||||||
|
@ -40,7 +42,9 @@ class AuxioApp : Application(), ImageLoaderFactory {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
// Migrate any settings that may have changed in an app update.
|
// 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
|
// Adding static shortcuts in a dynamic manner is better than declaring them
|
||||||
// manually, as it will properly handle the difference between debug and release
|
// manually, as it will properly handle the difference between debug and release
|
||||||
// Auxio instances.
|
// Auxio instances.
|
||||||
|
|
|
@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.system.IndexerService
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||||
import org.oxycblt.auxio.playback.system.PlaybackService
|
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.androidViewModels
|
||||||
import org.oxycblt.auxio.util.isNight
|
import org.oxycblt.auxio.util.isNight
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -81,7 +81,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupTheme() {
|
private fun setupTheme() {
|
||||||
val settings = Settings(this)
|
val settings = UISettings.from(this)
|
||||||
// Apply the theme configuration.
|
// Apply the theme configuration.
|
||||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||||
// Apply the color scheme. The black theme requires it's own set of themes since
|
// 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.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.Sort
|
import org.oxycblt.auxio.music.Sort
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -123,7 +123,7 @@ class AlbumDetailFragment :
|
||||||
|
|
||||||
override fun onRealClick(item: Music) {
|
override fun onRealClick(item: Music) {
|
||||||
val song = requireIs<Song>(item)
|
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
|
// "Play from shown item" and "Play from album" functionally have the same
|
||||||
// behavior since a song can only have one album.
|
// behavior since a song can only have one album.
|
||||||
null,
|
null,
|
||||||
|
@ -149,12 +149,12 @@ class AlbumDetailFragment :
|
||||||
|
|
||||||
override fun onOpenSortMenu(anchor: View) {
|
override fun onOpenSortMenu(anchor: View) {
|
||||||
openMenu(anchor, R.menu.menu_album_sort) {
|
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(sort.mode.itemId)).isChecked = true
|
||||||
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener { item ->
|
||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
detailModel.albumSort =
|
detailModel.albumSortSort =
|
||||||
if (item.itemId == R.id.option_sort_asc) {
|
if (item.itemId == R.id.option_sort_asc) {
|
||||||
sort.withAscending(item.isChecked)
|
sort.withAscending(item.isChecked)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.Sort
|
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.collect
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -123,7 +123,7 @@ class ArtistDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detai
|
||||||
override fun onRealClick(item: Music) {
|
override fun onRealClick(item: Music) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> {
|
is Song -> {
|
||||||
when (Settings(requireContext()).detailPlaybackMode) {
|
when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) {
|
||||||
// When configured to play from the selected item, we already have an Artist
|
// When configured to play from the selected item, we already have an Artist
|
||||||
// to play from.
|
// to play from.
|
||||||
null ->
|
null ->
|
||||||
|
@ -158,13 +158,13 @@ class ArtistDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detai
|
||||||
|
|
||||||
override fun onOpenSortMenu(anchor: View) {
|
override fun onOpenSortMenu(anchor: View) {
|
||||||
openMenu(anchor, R.menu.menu_artist_sort) {
|
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(sort.mode.itemId)).isChecked = true
|
||||||
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener { item ->
|
||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
|
|
||||||
detailModel.artistSort =
|
detailModel.artistSongSort =
|
||||||
if (item.itemId == R.id.option_sort_asc) {
|
if (item.itemId == R.id.option_sort_asc) {
|
||||||
sort.withAscending(item.isChecked)
|
sort.withAscending(item.isChecked)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -33,8 +33,10 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.list.Header
|
import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.music.*
|
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.music.storage.MimeType
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,7 +49,7 @@ import org.oxycblt.auxio.util.*
|
||||||
class DetailViewModel(application: Application) :
|
class DetailViewModel(application: Application) :
|
||||||
AndroidViewModel(application), MusicStore.Listener {
|
AndroidViewModel(application), MusicStore.Listener {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settings = Settings(application)
|
private val musicSettings = MusicSettings.from(application)
|
||||||
|
|
||||||
private var currentSongJob: Job? = null
|
private var currentSongJob: Job? = null
|
||||||
|
|
||||||
|
@ -75,10 +77,10 @@ class DetailViewModel(application: Application) :
|
||||||
get() = _albumList
|
get() = _albumList
|
||||||
|
|
||||||
/** The current [Sort] used for [Song]s in [albumList]. */
|
/** The current [Sort] used for [Song]s in [albumList]. */
|
||||||
var albumSort: Sort
|
var albumSortSort: Sort
|
||||||
get() = settings.detailAlbumSort
|
get() = musicSettings.albumSongSort
|
||||||
set(value) {
|
set(value) {
|
||||||
settings.detailAlbumSort = value
|
musicSettings.albumSongSort = value
|
||||||
// Refresh the album list to reflect the new sort.
|
// Refresh the album list to reflect the new sort.
|
||||||
currentAlbum.value?.let(::refreshAlbumList)
|
currentAlbum.value?.let(::refreshAlbumList)
|
||||||
}
|
}
|
||||||
|
@ -95,10 +97,10 @@ class DetailViewModel(application: Application) :
|
||||||
val artistList: StateFlow<List<Item>> = _artistList
|
val artistList: StateFlow<List<Item>> = _artistList
|
||||||
|
|
||||||
/** The current [Sort] used for [Song]s in [artistList]. */
|
/** The current [Sort] used for [Song]s in [artistList]. */
|
||||||
var artistSort: Sort
|
var artistSongSort: Sort
|
||||||
get() = settings.detailArtistSort
|
get() = musicSettings.artistSongSort
|
||||||
set(value) {
|
set(value) {
|
||||||
settings.detailArtistSort = value
|
musicSettings.artistSongSort = value
|
||||||
// Refresh the artist list to reflect the new sort.
|
// Refresh the artist list to reflect the new sort.
|
||||||
currentArtist.value?.let(::refreshArtistList)
|
currentArtist.value?.let(::refreshArtistList)
|
||||||
}
|
}
|
||||||
|
@ -115,10 +117,10 @@ class DetailViewModel(application: Application) :
|
||||||
val genreList: StateFlow<List<Item>> = _genreList
|
val genreList: StateFlow<List<Item>> = _genreList
|
||||||
|
|
||||||
/** The current [Sort] used for [Song]s in [genreList]. */
|
/** The current [Sort] used for [Song]s in [genreList]. */
|
||||||
var genreSort: Sort
|
var genreSongSort: Sort
|
||||||
get() = settings.detailGenreSort
|
get() = musicSettings.genreSongSort
|
||||||
set(value) {
|
set(value) {
|
||||||
settings.detailGenreSort = value
|
musicSettings.genreSongSort = value
|
||||||
// Refresh the genre list to reflect the new sort.
|
// Refresh the genre list to reflect the new sort.
|
||||||
currentGenre.value?.let(::refreshGenreList)
|
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
|
// 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.
|
// 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.
|
// Songs without disc tags become part of Disc 1.
|
||||||
val byDisc = songs.groupBy { it.disc ?: 1 }
|
val byDisc = songs.groupBy { it.disc ?: 1 }
|
||||||
if (byDisc.size > 1) {
|
if (byDisc.size > 1) {
|
||||||
|
@ -363,7 +365,7 @@ class DetailViewModel(application: Application) :
|
||||||
if (artist.songs.isNotEmpty()) {
|
if (artist.songs.isNotEmpty()) {
|
||||||
logD("Songs present in this artist, adding header")
|
logD("Songs present in this artist, adding header")
|
||||||
data.add(SortHeader(R.string.lbl_songs))
|
data.add(SortHeader(R.string.lbl_songs))
|
||||||
data.addAll(artistSort.songs(artist.songs))
|
data.addAll(artistSongSort.songs(artist.songs))
|
||||||
}
|
}
|
||||||
|
|
||||||
_artistList.value = data.toList()
|
_artistList.value = data.toList()
|
||||||
|
@ -376,7 +378,7 @@ class DetailViewModel(application: Application) :
|
||||||
data.add(Header(R.string.lbl_artists))
|
data.add(Header(R.string.lbl_artists))
|
||||||
data.addAll(genre.artists)
|
data.addAll(genre.artists)
|
||||||
data.add(SortHeader(R.string.lbl_songs))
|
data.add(SortHeader(R.string.lbl_songs))
|
||||||
data.addAll(genreSort.songs(genre.songs))
|
data.addAll(genreSongSort.songs(genre.songs))
|
||||||
_genreList.value = data
|
_genreList.value = data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.Sort
|
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.collect
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -123,7 +123,7 @@ class GenreDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detail
|
||||||
when (item) {
|
when (item) {
|
||||||
is Artist -> navModel.exploreNavigateTo(item)
|
is Artist -> navModel.exploreNavigateTo(item)
|
||||||
is Song ->
|
is Song ->
|
||||||
when (Settings(requireContext()).detailPlaybackMode) {
|
when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) {
|
||||||
// When configured to play from the selected item, we already have a Genre
|
// When configured to play from the selected item, we already have a Genre
|
||||||
// to play from.
|
// to play from.
|
||||||
null ->
|
null ->
|
||||||
|
@ -156,12 +156,12 @@ class GenreDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detail
|
||||||
|
|
||||||
override fun onOpenSortMenu(anchor: View) {
|
override fun onOpenSortMenu(anchor: View) {
|
||||||
openMenu(anchor, R.menu.menu_genre_sort) {
|
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(sort.mode.itemId)).isChecked = true
|
||||||
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener { item ->
|
||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
detailModel.genreSort =
|
detailModel.genreSongSort =
|
||||||
if (item.itemId == R.id.option_sort_asc) {
|
if (item.itemId == R.id.option_sort_asc) {
|
||||||
sort.withAscending(item.isChecked)
|
sort.withAscending(item.isChecked)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -50,6 +50,8 @@ import org.oxycblt.auxio.home.list.SongListFragment
|
||||||
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
|
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
|
||||||
import org.oxycblt.auxio.list.selection.SelectionFragment
|
import org.oxycblt.auxio.list.selection.SelectionFragment
|
||||||
import org.oxycblt.auxio.music.*
|
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.music.system.Indexer
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
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.R
|
||||||
import org.oxycblt.auxio.home.tabs.Tab
|
import org.oxycblt.auxio.home.tabs.Tab
|
||||||
import org.oxycblt.auxio.music.*
|
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.context
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -38,7 +40,8 @@ class HomeViewModel(application: Application) :
|
||||||
MusicStore.Listener,
|
MusicStore.Listener,
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
private val musicStore = MusicStore.getInstance()
|
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>())
|
private val _songsList = MutableStateFlow(listOf<Song>())
|
||||||
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
|
/** 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 {
|
init {
|
||||||
musicStore.addListener(this)
|
musicStore.addListener(this)
|
||||||
settings.addListener(this)
|
homeSettings.addListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
musicStore.removeListener(this)
|
musicStore.removeListener(this)
|
||||||
settings.removeListener(this)
|
homeSettings.removeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLibraryChanged(library: Library?) {
|
override fun onLibraryChanged(library: Library?) {
|
||||||
|
@ -103,17 +106,17 @@ class HomeViewModel(application: Application) :
|
||||||
logD("Library changed, refreshing library")
|
logD("Library changed, refreshing library")
|
||||||
// Get the each list of items in the library to use as our list data.
|
// Get the each list of items in the library to use as our list data.
|
||||||
// Applying the preferred sorting to them.
|
// Applying the preferred sorting to them.
|
||||||
_songsList.value = settings.libSongSort.songs(library.songs)
|
_songsList.value = musicSettings.songSort.songs(library.songs)
|
||||||
_albumsLists.value = settings.libAlbumSort.albums(library.albums)
|
_albumsLists.value = musicSettings.albumSort.albums(library.albums)
|
||||||
_artistsList.value =
|
_artistsList.value =
|
||||||
settings.libArtistSort.artists(
|
musicSettings.artistSort.artists(
|
||||||
if (settings.shouldHideCollaborators) {
|
if (homeSettings.shouldHideCollaborators) {
|
||||||
// Hide Collaborators is enabled, filter out collaborators.
|
// Hide Collaborators is enabled, filter out collaborators.
|
||||||
library.artists.filter { !it.isCollaborator }
|
library.artists.filter { !it.isCollaborator }
|
||||||
} else {
|
} else {
|
||||||
library.artists
|
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) =
|
fun getSortForTab(tabMode: MusicMode) =
|
||||||
when (tabMode) {
|
when (tabMode) {
|
||||||
MusicMode.SONGS -> settings.libSongSort
|
MusicMode.SONGS -> musicSettings.songSort
|
||||||
MusicMode.ALBUMS -> settings.libAlbumSort
|
MusicMode.ALBUMS -> musicSettings.albumSort
|
||||||
MusicMode.ARTISTS -> settings.libArtistSort
|
MusicMode.ARTISTS -> musicSettings.artistSort
|
||||||
MusicMode.GENRES -> settings.libGenreSort
|
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.
|
// Can simply re-sort the current list of items without having to access the library.
|
||||||
when (_currentTabMode.value) {
|
when (_currentTabMode.value) {
|
||||||
MusicMode.SONGS -> {
|
MusicMode.SONGS -> {
|
||||||
settings.libSongSort = sort
|
musicSettings.songSort = sort
|
||||||
_songsList.value = sort.songs(_songsList.value)
|
_songsList.value = sort.songs(_songsList.value)
|
||||||
}
|
}
|
||||||
MusicMode.ALBUMS -> {
|
MusicMode.ALBUMS -> {
|
||||||
settings.libAlbumSort = sort
|
musicSettings.albumSort = sort
|
||||||
_albumsLists.value = sort.albums(_albumsLists.value)
|
_albumsLists.value = sort.albums(_albumsLists.value)
|
||||||
}
|
}
|
||||||
MusicMode.ARTISTS -> {
|
MusicMode.ARTISTS -> {
|
||||||
settings.libArtistSort = sort
|
musicSettings.artistSort = sort
|
||||||
_artistsList.value = sort.artists(_artistsList.value)
|
_artistsList.value = sort.artists(_artistsList.value)
|
||||||
}
|
}
|
||||||
MusicMode.GENRES -> {
|
MusicMode.GENRES -> {
|
||||||
settings.libGenreSort = sort
|
musicSettings.genreSort = sort
|
||||||
_genresList.value = sort.genres(_genresList.value)
|
_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
|
* @return A list of the [MusicMode]s for each visible [Tab] in the configuration, ordered in
|
||||||
* the same way as the configuration.
|
* 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.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
|
import org.oxycblt.auxio.music.Sort
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.playback.secsToMs
|
import org.oxycblt.auxio.playback.secsToMs
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
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.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.Sort
|
import org.oxycblt.auxio.music.Sort
|
||||||
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.playback.secsToMs
|
import org.oxycblt.auxio.playback.secsToMs
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -130,7 +130,7 @@ class SongListFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRealClick(item: Song) {
|
override fun onRealClick(item: Song) {
|
||||||
when (Settings(requireContext()).libPlaybackMode) {
|
when (PlaybackSettings.from(requireContext()).inListPlaybackMode) {
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
||||||
|
|
|
@ -25,8 +25,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogTabsBinding
|
import org.oxycblt.auxio.databinding.DialogTabsBinding
|
||||||
|
import org.oxycblt.auxio.home.HomeSettings
|
||||||
import org.oxycblt.auxio.list.EditableListListener
|
import org.oxycblt.auxio.list.EditableListListener
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -46,13 +46,13 @@ class TabCustomizeDialog :
|
||||||
.setTitle(R.string.set_lib_tabs)
|
.setTitle(R.string.set_lib_tabs)
|
||||||
.setPositiveButton(R.string.lbl_ok) { _, _ ->
|
.setPositiveButton(R.string.lbl_ok) { _, _ ->
|
||||||
logD("Committing tab changes")
|
logD("Committing tab changes")
|
||||||
Settings(requireContext()).libTabs = tabAdapter.tabs
|
HomeSettings.from(requireContext()).homeTabs = tabAdapter.tabs
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.lbl_cancel, null)
|
.setNegativeButton(R.string.lbl_cancel, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) {
|
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.
|
// Try to restore a pending tab configuration that was saved prior.
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
val savedTabs = Tab.fromIntCode(savedInstanceState.getInt(KEY_TABS))
|
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 com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import org.oxycblt.auxio.R
|
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.getColorCompat
|
||||||
import org.oxycblt.auxio.util.getDrawableCompat
|
import org.oxycblt.auxio.util.getDrawableCompat
|
||||||
|
|
||||||
|
@ -52,7 +52,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
private val indicatorMatrix = Matrix()
|
private val indicatorMatrix = Matrix()
|
||||||
private val indicatorMatrixSrc = RectF()
|
private val indicatorMatrixSrc = RectF()
|
||||||
private val indicatorMatrixDst = 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
|
* 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) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
(background as? MaterialShapeDrawable)?.let { bg ->
|
(background as? MaterialShapeDrawable)?.let { bg ->
|
||||||
if (settings.roundMode) {
|
if (UISettings.from(context).roundMode) {
|
||||||
bg.setCornerSize(value)
|
bg.setCornerSize(value)
|
||||||
} else {
|
} else {
|
||||||
bg.setCornerSize(0f)
|
bg.setCornerSize(0f)
|
||||||
|
|
|
@ -39,7 +39,7 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
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.getColorCompat
|
||||||
import org.oxycblt.auxio.util.getDrawableCompat
|
import org.oxycblt.auxio.util.getDrawableCompat
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
background =
|
background =
|
||||||
MaterialShapeDrawable().apply {
|
MaterialShapeDrawable().apply {
|
||||||
fillColor = context.getColorCompat(R.color.sel_cover_bg)
|
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.
|
// Only use the specified corner radius when round mode is enabled.
|
||||||
setCornerSize(cornerRadius)
|
setCornerSize(cornerRadius)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,8 +29,8 @@ import java.io.InputStream
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.image.CoverMode
|
import org.oxycblt.auxio.image.CoverMode
|
||||||
|
import org.oxycblt.auxio.image.ImageSettings
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
|
@ -47,10 +47,8 @@ object Covers {
|
||||||
* loading failed or should not occur.
|
* loading failed or should not occur.
|
||||||
*/
|
*/
|
||||||
suspend fun fetch(context: Context, album: Album): InputStream? {
|
suspend fun fetch(context: Context, album: Album): InputStream? {
|
||||||
val settings = Settings(context)
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
when (settings.coverMode) {
|
when (ImageSettings.from(context).coverMode) {
|
||||||
CoverMode.OFF -> null
|
CoverMode.OFF -> null
|
||||||
CoverMode.MEDIA_STORE -> fetchMediaStoreCovers(context, album)
|
CoverMode.MEDIA_STORE -> fetchMediaStoreCovers(context, album)
|
||||||
CoverMode.QUALITY -> fetchQualityCovers(context, album)
|
CoverMode.QUALITY -> fetchQualityCovers(context, album)
|
||||||
|
|
|
@ -21,6 +21,8 @@ import androidx.lifecycle.ViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
|
import org.oxycblt.auxio.music.Library
|
||||||
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [ViewModel] that manages the current selection.
|
* A [ViewModel] that manages the current selection.
|
||||||
|
|
|
@ -20,9 +20,9 @@ package org.oxycblt.auxio.music
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.storage.contentResolverSafe
|
import org.oxycblt.auxio.music.storage.contentResolverSafe
|
||||||
import org.oxycblt.auxio.music.storage.useQuery
|
import org.oxycblt.auxio.music.storage.useQuery
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,7 +33,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart
|
* @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. */
|
/** All [Song]s that were detected on the device. */
|
||||||
val songs = Sort(Sort.Mode.ByName, true).songs(rawSongs.map { Song(it, settings) })
|
val songs = Sort(Sort.Mode.ByName, true).songs(rawSongs.map { Song(it, settings) })
|
||||||
/** All [Album]s found on the device. */
|
/** 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.parseId3GenreNames
|
||||||
import org.oxycblt.auxio.music.parsing.parseMultiValue
|
import org.oxycblt.auxio.music.parsing.parseMultiValue
|
||||||
import org.oxycblt.auxio.music.storage.*
|
import org.oxycblt.auxio.music.storage.*
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
|
@ -308,10 +307,10 @@ sealed class MusicParent : Music() {
|
||||||
/**
|
/**
|
||||||
* A song. Perhaps the foundation of the entirety of Auxio.
|
* A song. Perhaps the foundation of the entirety of Auxio.
|
||||||
* @param raw The [Song.Raw] to derive the member data from.
|
* @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)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class Song constructor(raw: Raw, settings: Settings) : Music() {
|
class Song constructor(raw: Raw, musicSettings: MusicSettings) : Music() {
|
||||||
override val uid =
|
override val uid =
|
||||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||||
raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) }
|
raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) }
|
||||||
|
@ -381,10 +380,9 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
val album: Album
|
val album: Album
|
||||||
get() = unlikelyToBeNull(_album)
|
get() = unlikelyToBeNull(_album)
|
||||||
|
|
||||||
private val artistMusicBrainzIds =
|
private val artistMusicBrainzIds = raw.artistMusicBrainzIds.parseMultiValue(musicSettings)
|
||||||
raw.artistMusicBrainzIds.parseMultiValue(settings.musicSeparators)
|
private val artistNames = raw.artistNames.parseMultiValue(musicSettings)
|
||||||
private val artistNames = raw.artistNames.parseMultiValue(settings.musicSeparators)
|
private val artistSortNames = raw.artistSortNames.parseMultiValue(musicSettings)
|
||||||
private val artistSortNames = raw.artistSortNames.parseMultiValue(settings.musicSeparators)
|
|
||||||
private val rawArtists =
|
private val rawArtists =
|
||||||
artistNames.mapIndexed { i, name ->
|
artistNames.mapIndexed { i, name ->
|
||||||
Artist.Raw(
|
Artist.Raw(
|
||||||
|
@ -394,10 +392,9 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val albumArtistMusicBrainzIds =
|
private val albumArtistMusicBrainzIds =
|
||||||
raw.albumArtistMusicBrainzIds.parseMultiValue(settings.musicSeparators)
|
raw.albumArtistMusicBrainzIds.parseMultiValue(musicSettings)
|
||||||
private val albumArtistNames = raw.albumArtistNames.parseMultiValue(settings.musicSeparators)
|
private val albumArtistNames = raw.albumArtistNames.parseMultiValue(musicSettings)
|
||||||
private val albumArtistSortNames =
|
private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(musicSettings)
|
||||||
raw.albumArtistSortNames.parseMultiValue(settings.musicSeparators)
|
|
||||||
private val rawAlbumArtists =
|
private val rawAlbumArtists =
|
||||||
albumArtistNames.mapIndexed { i, name ->
|
albumArtistNames.mapIndexed { i, name ->
|
||||||
Artist.Raw(
|
Artist.Raw(
|
||||||
|
@ -465,7 +462,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
|
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
|
||||||
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
||||||
sortName = raw.albumSortName,
|
sortName = raw.albumSortName,
|
||||||
type = Album.Type.parse(raw.albumTypes.parseMultiValue(settings.musicSeparators)),
|
type = Album.Type.parse(raw.albumTypes.parseMultiValue(musicSettings)),
|
||||||
rawArtists =
|
rawArtists =
|
||||||
rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
|
rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
|
||||||
|
|
||||||
|
@ -484,7 +481,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
*/
|
*/
|
||||||
val _rawGenres =
|
val _rawGenres =
|
||||||
raw.genreNames
|
raw.genreNames
|
||||||
.parseId3GenreNames(settings.musicSeparators)
|
.parseId3GenreNames(musicSettings)
|
||||||
.map { Genre.Raw(it) }
|
.map { Genre.Raw(it) }
|
||||||
.ifEmpty { listOf(Genre.Raw()) }
|
.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 androidx.core.database.getStringOrNull
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.oxycblt.auxio.music.Date
|
import org.oxycblt.auxio.music.Date
|
||||||
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.parsing.parseId3v2Position
|
import org.oxycblt.auxio.music.parsing.parseId3v2Position
|
||||||
import org.oxycblt.auxio.music.storage.Directory
|
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.safeQuery
|
||||||
import org.oxycblt.auxio.music.storage.storageVolumesCompat
|
import org.oxycblt.auxio.music.storage.storageVolumesCompat
|
||||||
import org.oxycblt.auxio.music.storage.useQuery
|
import org.oxycblt.auxio.music.storage.useQuery
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
|
@ -86,20 +86,20 @@ abstract class MediaStoreExtractor(
|
||||||
open fun init(): Cursor {
|
open fun init(): Cursor {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
cacheExtractor.init()
|
cacheExtractor.init()
|
||||||
val settings = Settings(context)
|
val musicSettings = MusicSettings.from(context)
|
||||||
val storageManager = context.getSystemServiceCompat(StorageManager::class)
|
val storageManager = context.getSystemServiceCompat(StorageManager::class)
|
||||||
|
|
||||||
val args = mutableListOf<String>()
|
val args = mutableListOf<String>()
|
||||||
var selector = BASE_SELECTOR
|
var selector = BASE_SELECTOR
|
||||||
|
|
||||||
// Filter out audio that is not music, if enabled.
|
// Filter out audio that is not music, if enabled.
|
||||||
if (settings.excludeNonMusic) {
|
if (musicSettings.excludeNonMusic) {
|
||||||
logD("Excluding non-music")
|
logD("Excluding non-music")
|
||||||
selector += " AND ${MediaStore.Audio.AudioColumns.IS_MUSIC}=1"
|
selector += " AND ${MediaStore.Audio.AudioColumns.IS_MUSIC}=1"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the projection to follow the music directory configuration.
|
// Set up the projection to follow the music directory configuration.
|
||||||
val dirs = settings.getMusicDirs(storageManager)
|
val dirs = musicSettings.musicDirs
|
||||||
if (dirs.dirs.isNotEmpty()) {
|
if (dirs.dirs.isNotEmpty()) {
|
||||||
selector += " AND "
|
selector += " AND "
|
||||||
if (!dirs.shouldInclude) {
|
if (!dirs.shouldInclude) {
|
||||||
|
|
|
@ -230,7 +230,7 @@ class Task(context: Context, private val raw: Song.Raw) {
|
||||||
* Frames.
|
* Frames.
|
||||||
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
|
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
|
||||||
* values.
|
* 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
|
* 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.
|
* 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
|
package org.oxycblt.auxio.music.parsing
|
||||||
|
|
||||||
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
|
|
||||||
/// --- GENERIC PARSING ---
|
/// --- 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
|
* 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
|
* than one value, nothing is done. Otherwise, this function will attempt to split it based on the
|
||||||
* user's separator preferences.
|
* 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.
|
* @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) {
|
if (size == 1) {
|
||||||
first().maybeParseBySeparators(separators)
|
first().maybeParseBySeparators(settings)
|
||||||
} else {
|
} else {
|
||||||
// Nothing to do.
|
// Nothing to do.
|
||||||
this
|
this
|
||||||
|
@ -82,7 +83,7 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): List<String> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fix trailing whitespace or blank contents in a [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.
|
* empty.
|
||||||
*/
|
*/
|
||||||
fun String.correctWhitespace() = trim().ifBlank { null }
|
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.
|
* Attempt to parse a string by the user's separator preferences.
|
||||||
* @param separators A string of characters to split by. Can be empty.
|
* @param settings [Settings] required to obtain user separator configuration.
|
||||||
* @return A list of one or more [String]s that were split up by the given separators.
|
* @return A list of one or more [String]s that were split up by the user-defined separators.
|
||||||
*/
|
*/
|
||||||
private fun String.maybeParseBySeparators(separators: String) =
|
private fun String.maybeParseBySeparators(settings: MusicSettings): List<String> {
|
||||||
if (separators.isNotEmpty()) {
|
// Get the separators the user desires. If null, there's nothing to do.
|
||||||
splitEscaped { separators.contains(it) }.correctWhitespace()
|
return splitEscaped { settings.multiValueSeparators.contains(it) }.correctWhitespace()
|
||||||
} else {
|
|
||||||
listOf(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// --- ID3v2 PARSING ---
|
/// --- 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
|
* 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
|
* representations of genre fields into their named counterparts, and split up singular ID3v2-style
|
||||||
* integer genre fields into one or more genres.
|
* integer genre fields into one or more genres.
|
||||||
* @param separators A string of characters to split by. Can be empty.
|
* @param settings [MusicSettings] required to obtain user separator configuration.
|
||||||
* @return A list of one or more genre names.
|
* @return A list of one or more genre names..
|
||||||
*/
|
*/
|
||||||
fun List<String>.parseId3GenreNames(separators: String) =
|
fun List<String>.parseId3GenreNames(settings: MusicSettings) =
|
||||||
if (size == 1) {
|
if (size == 1) {
|
||||||
first().parseId3MultiValueGenre(separators)
|
first().parseId3MultiValueGenre(settings)
|
||||||
} else {
|
} else {
|
||||||
// Nothing to split, just map any ID3v1 genres to their name counterparts.
|
// Nothing to split, just map any ID3v1 genres to their name counterparts.
|
||||||
map { it.parseId3v1Genre() ?: it }
|
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? {
|
private fun String.parseId3v1Genre(): String? {
|
||||||
// ID3v1 genres are a plain integer value without formatting, so in that case
|
// ID3v1 genres are a plain integer value without formatting, so in that case
|
||||||
// try to index the genre table with such.
|
// 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)\\))*)(.+)?")
|
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>? {
|
private fun String.parseId3v2Genre(): List<String>? {
|
||||||
val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues
|
val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues
|
||||||
val genres = mutableSetOf<String>()
|
val genres = mutableSetOf<String>()
|
||||||
|
|
|
@ -25,7 +25,7 @@ import com.google.android.material.checkbox.MaterialCheckBox
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
|
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,7 +42,7 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
|
||||||
.setTitle(R.string.set_separators)
|
.setTitle(R.string.set_separators)
|
||||||
.setNegativeButton(R.string.lbl_cancel, null)
|
.setNegativeButton(R.string.lbl_cancel, null)
|
||||||
.setPositiveButton(R.string.lbl_save) { _, _ ->
|
.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
|
// the corresponding CheckBox for each character instead of doing an iteration
|
||||||
// through the separator list for each CheckBox.
|
// through the separator list for each CheckBox.
|
||||||
(savedInstanceState?.getString(KEY_PENDING_SEPARATORS)
|
(savedInstanceState?.getString(KEY_PENDING_SEPARATORS)
|
||||||
?: Settings(requireContext()).musicSeparators)
|
?: MusicSettings.from(requireContext()).multiValueSeparators)
|
||||||
?.forEach {
|
.forEach {
|
||||||
when (it) {
|
when (it) {
|
||||||
Separators.COMMA -> binding.separatorComma.isChecked = true
|
Separators.COMMA -> binding.separatorComma.isChecked = true
|
||||||
Separators.SEMICOLON -> binding.separatorSemicolon.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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
|
import org.oxycblt.auxio.music.Library
|
||||||
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -30,7 +30,7 @@ import androidx.core.view.isVisible
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
|
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.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -56,14 +56,11 @@ class MusicDirsDialog :
|
||||||
.setNeutralButton(R.string.lbl_add, null)
|
.setNeutralButton(R.string.lbl_add, null)
|
||||||
.setNegativeButton(R.string.lbl_cancel, null)
|
.setNegativeButton(R.string.lbl_cancel, null)
|
||||||
.setPositiveButton(R.string.lbl_save) { _, _ ->
|
.setPositiveButton(R.string.lbl_save) { _, _ ->
|
||||||
val settings = Settings(requireContext())
|
val settings = MusicSettings.from(requireContext())
|
||||||
val dirs =
|
|
||||||
settings.getMusicDirs(
|
|
||||||
requireNotNull(storageManager) { "StorageManager was not available" })
|
|
||||||
val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
|
val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
|
||||||
if (dirs != newDirs) {
|
if (settings.musicDirs != newDirs) {
|
||||||
logD("Committing changes")
|
logD("Committing changes")
|
||||||
settings.setMusicDirs(newDirs)
|
settings.musicDirs = newDirs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,7 +101,7 @@ class MusicDirsDialog :
|
||||||
itemAnimator = null
|
itemAnimator = null
|
||||||
}
|
}
|
||||||
|
|
||||||
var dirs = Settings(context).getMusicDirs(storageManager)
|
var dirs = MusicSettings.from(context).musicDirs
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS)
|
val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS)
|
||||||
if (pendingDirs != null) {
|
if (pendingDirs != null) {
|
||||||
|
|
|
@ -28,8 +28,8 @@ import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
|
import org.oxycblt.auxio.music.Library
|
||||||
import org.oxycblt.auxio.music.extractor.*
|
import org.oxycblt.auxio.music.extractor.*
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.logW
|
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
|
// 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.
|
// and reliable compared to using MediaStore to obtain grouping information.
|
||||||
val buildStart = System.currentTimeMillis()
|
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")
|
logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
|
||||||
return library
|
return library
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,11 +33,11 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.storage.contentResolverSafe
|
import org.oxycblt.auxio.music.storage.contentResolverSafe
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.service.ForegroundManager
|
import org.oxycblt.auxio.service.ForegroundManager
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ class IndexerService :
|
||||||
private lateinit var observingNotification: ObservingNotification
|
private lateinit var observingNotification: ObservingNotification
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
private lateinit var indexerContentObserver: SystemContentObserver
|
private lateinit var indexerContentObserver: SystemContentObserver
|
||||||
private lateinit var settings: Settings
|
private lateinit var settings: MusicSettings
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
@ -83,7 +83,7 @@ class IndexerService :
|
||||||
// Initialize any listener-dependent components last as we wouldn't want a listener race
|
// 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.
|
// condition to cause us to load music before we were fully initialize.
|
||||||
indexerContentObserver = SystemContentObserver()
|
indexerContentObserver = SystemContentObserver()
|
||||||
settings = Settings(this)
|
settings = MusicSettings.from(this)
|
||||||
settings.addListener(this)
|
settings.addListener(this)
|
||||||
indexer.registerController(this)
|
indexer.registerController(this)
|
||||||
// An indeterminate indexer and a missing library implies we are extremely early
|
// 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.databinding.FragmentPlaybackBarBinding
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
|
@ -66,7 +65,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
||||||
|
|
||||||
// Set up actions
|
// Set up actions
|
||||||
binding.playbackPlayPause.setOnClickListener { playbackModel.toggleIsPlaying() }
|
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
|
// Load the track color in manually as it's unclear whether the track actually supports
|
||||||
// using a ColorStateList in the resources.
|
// using a ColorStateList in the resources.
|
||||||
|
@ -86,8 +85,8 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
||||||
binding.playbackInfo.isSelected = false
|
binding.playbackInfo.isSelected = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, settings: Settings) {
|
private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, actionMode: ActionMode) {
|
||||||
when (settings.playbackBarAction) {
|
when (actionMode) {
|
||||||
ActionMode.NEXT -> {
|
ActionMode.NEXT -> {
|
||||||
binding.playbackSecondaryAction.apply {
|
binding.playbackSecondaryAction.apply {
|
||||||
setIconResource(R.drawable.ic_skip_next_24)
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.oxycblt.auxio.home.HomeSettings
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.playback.state.*
|
import org.oxycblt.auxio.playback.state.*
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,7 +36,9 @@ import org.oxycblt.auxio.util.context
|
||||||
*/
|
*/
|
||||||
class PlaybackViewModel(application: Application) :
|
class PlaybackViewModel(application: Application) :
|
||||||
AndroidViewModel(application), PlaybackStateManager.Listener {
|
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 val playbackManager = PlaybackStateManager.getInstance()
|
||||||
private var lastPositionJob: Job? = null
|
private var lastPositionJob: Job? = null
|
||||||
|
|
||||||
|
@ -249,17 +251,17 @@ class PlaybackViewModel(application: Application) :
|
||||||
private fun playImpl(
|
private fun playImpl(
|
||||||
song: Song?,
|
song: Song?,
|
||||||
parent: MusicParent?,
|
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)) {
|
check(song == null || parent == null || parent.songs.contains(song)) {
|
||||||
"Song to play not in parent"
|
"Song to play not in parent"
|
||||||
}
|
}
|
||||||
val sort =
|
val sort =
|
||||||
when (parent) {
|
when (parent) {
|
||||||
is Genre -> settings.detailGenreSort
|
is Genre -> musicSettings.genreSongSort
|
||||||
is Artist -> settings.detailArtistSort
|
is Artist -> musicSettings.artistSongSort
|
||||||
is Album -> settings.detailAlbumSort
|
is Album -> musicSettings.albumSongSort
|
||||||
null -> settings.libSongSort
|
null -> musicSettings.songSort
|
||||||
}
|
}
|
||||||
playbackManager.play(song, parent, sort, shuffled)
|
playbackManager.play(song, parent, sort, shuffled)
|
||||||
}
|
}
|
||||||
|
@ -301,7 +303,7 @@ class PlaybackViewModel(application: Application) :
|
||||||
* @param album The [Album] to add.
|
* @param album The [Album] to add.
|
||||||
*/
|
*/
|
||||||
fun playNext(album: Album) {
|
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.
|
* @param artist The [Artist] to add.
|
||||||
*/
|
*/
|
||||||
fun playNext(artist: Artist) {
|
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.
|
* @param genre The [Genre] to add.
|
||||||
*/
|
*/
|
||||||
fun playNext(genre: Genre) {
|
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.
|
* @param album The [Album] to add.
|
||||||
*/
|
*/
|
||||||
fun addToQueue(album: Album) {
|
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.
|
* @param artist The [Artist] to add.
|
||||||
*/
|
*/
|
||||||
fun addToQueue(artist: Artist) {
|
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.
|
* @param genre The [Genre] to add.
|
||||||
*/
|
*/
|
||||||
fun addToQueue(genre: Genre) {
|
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> {
|
private fun selectionToSongs(selection: List<Music>): List<Song> {
|
||||||
return selection.flatMap {
|
return selection.flatMap {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Album -> settings.detailAlbumSort.songs(it.songs)
|
is Album -> musicSettings.albumSongSort.songs(it.songs)
|
||||||
is Artist -> settings.detailArtistSort.songs(it.songs)
|
is Artist -> musicSettings.artistSongSort.songs(it.songs)
|
||||||
is Genre -> settings.detailGenreSort.songs(it.songs)
|
is Genre -> musicSettings.genreSongSort.songs(it.songs)
|
||||||
is Song -> listOf(it)
|
is Song -> listOf(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import androidx.appcompat.app.AlertDialog
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogPreAmpBinding
|
import org.oxycblt.auxio.databinding.DialogPreAmpBinding
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,11 +39,11 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
|
||||||
.setTitle(R.string.set_pre_amp)
|
.setTitle(R.string.set_pre_amp)
|
||||||
.setPositiveButton(R.string.lbl_ok) { _, _ ->
|
.setPositiveButton(R.string.lbl_ok) { _, _ ->
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
Settings(requireContext()).replayGainPreAmp =
|
PlaybackSettings.from(requireContext()).replayGainPreAmp =
|
||||||
ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value)
|
ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value)
|
||||||
}
|
}
|
||||||
.setNeutralButton(R.string.lbl_reset) { _, _ ->
|
.setNeutralButton(R.string.lbl_reset) { _, _ ->
|
||||||
Settings(requireContext()).replayGainPreAmp = ReplayGainPreAmp(0f, 0f)
|
PlaybackSettings.from(requireContext()).replayGainPreAmp = ReplayGainPreAmp(0f, 0f)
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.lbl_cancel, null)
|
.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
|
// 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
|
// settings. After this, the sliders save their own state, so we do not need to
|
||||||
// do any restore behavior.
|
// do any restore behavior.
|
||||||
val preAmp = Settings(requireContext()).replayGainPreAmp
|
val preAmp = PlaybackSettings.from(requireContext()).replayGainPreAmp
|
||||||
binding.withTagsSlider.value = preAmp.with
|
binding.withTagsSlider.value = preAmp.with
|
||||||
binding.withoutTagsSlider.value = preAmp.without
|
binding.withoutTagsSlider.value = preAmp.without
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,8 @@ import kotlin.math.pow
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.extractor.TextTags
|
import org.oxycblt.auxio.music.extractor.TextTags
|
||||||
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,7 +48,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
class ReplayGainAudioProcessor(private val context: Context) :
|
class ReplayGainAudioProcessor(private val context: Context) :
|
||||||
BaseAudioProcessor(), Player.Listener, SharedPreferences.OnSharedPreferenceChangeListener {
|
BaseAudioProcessor(), Player.Listener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
private val settings = Settings(context)
|
private val settings = PlaybackSettings.from(context)
|
||||||
private var lastFormat: Format? = null
|
private var lastFormat: Format? = null
|
||||||
|
|
||||||
private var volume = 1f
|
private var volume = 1f
|
||||||
|
|
|
@ -24,6 +24,7 @@ import android.database.sqlite.SQLiteOpenHelper
|
||||||
import android.provider.BaseColumns
|
import android.provider.BaseColumns
|
||||||
import androidx.core.database.sqlite.transaction
|
import androidx.core.database.sqlite.transaction
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
|
import org.oxycblt.auxio.music.Library
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,6 +21,9 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.*
|
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.playback.state.PlaybackStateManager.Listener
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
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.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.ActionMode
|
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.InternalPlayer
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.Queue
|
import org.oxycblt.auxio.playback.state.Queue
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.logD
|
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 playbackManager = PlaybackStateManager.getInstance()
|
||||||
private val settings = Settings(context)
|
private val settings = PlaybackSettings.from(context)
|
||||||
|
|
||||||
private val notification = NotificationComponent(context, mediaSession.sessionToken)
|
private val notification = NotificationComponent(context, mediaSession.sessionToken)
|
||||||
private val provider = BitmapProvider(context)
|
private val provider = BitmapProvider(context)
|
||||||
|
|
|
@ -44,15 +44,16 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.Library
|
import org.oxycblt.auxio.music.Library
|
||||||
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
||||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
|
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.service.ForegroundManager
|
import org.oxycblt.auxio.service.ForegroundManager
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.widgets.WidgetComponent
|
import org.oxycblt.auxio.widgets.WidgetComponent
|
||||||
import org.oxycblt.auxio.widgets.WidgetProvider
|
import org.oxycblt.auxio.widgets.WidgetProvider
|
||||||
|
@ -92,7 +93,8 @@ class PlaybackService :
|
||||||
// Managers
|
// Managers
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private lateinit var settings: Settings
|
private lateinit var musicSettings: MusicSettings
|
||||||
|
private lateinit var playbackSettings: PlaybackSettings
|
||||||
|
|
||||||
// State
|
// State
|
||||||
private lateinit var foregroundManager: ForegroundManager
|
private lateinit var foregroundManager: ForegroundManager
|
||||||
|
@ -143,7 +145,8 @@ class PlaybackService :
|
||||||
.also { it.addListener(this) }
|
.also { it.addListener(this) }
|
||||||
replayGainProcessor.addToListeners(player)
|
replayGainProcessor.addToListeners(player)
|
||||||
// Initialize the core service components
|
// Initialize the core service components
|
||||||
settings = Settings(this)
|
musicSettings = MusicSettings.from(this)
|
||||||
|
playbackSettings = PlaybackSettings.from(this)
|
||||||
foregroundManager = ForegroundManager(this)
|
foregroundManager = ForegroundManager(this)
|
||||||
// Initialize any listener-dependent components last as we wouldn't want a listener race
|
// 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.
|
// condition to cause us to load music before we were fully initialize.
|
||||||
|
@ -213,7 +216,7 @@ class PlaybackService :
|
||||||
get() = player.audioSessionId
|
get() = player.audioSessionId
|
||||||
|
|
||||||
override val shouldRewindWithPrev: Boolean
|
override val shouldRewindWithPrev: Boolean
|
||||||
get() = settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
|
get() = playbackSettings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
|
||||||
|
|
||||||
override fun getState(durationMs: Long) =
|
override fun getState(durationMs: Long) =
|
||||||
InternalPlayer.State.from(
|
InternalPlayer.State.from(
|
||||||
|
@ -286,7 +289,7 @@ class PlaybackService :
|
||||||
if (playbackManager.repeatMode == RepeatMode.TRACK) {
|
if (playbackManager.repeatMode == RepeatMode.TRACK) {
|
||||||
playbackManager.rewind()
|
playbackManager.rewind()
|
||||||
// May be configured to pause when we repeat a track.
|
// May be configured to pause when we repeat a track.
|
||||||
if (settings.pauseOnRepeat) {
|
if (playbackSettings.pauseOnRepeat) {
|
||||||
playbackManager.setPlaying(false)
|
playbackManager.setPlaying(false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -352,7 +355,7 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
// Shuffle all -> Start new playback from all songs
|
// Shuffle all -> Start new playback from all songs
|
||||||
is InternalPlayer.Action.ShuffleAll -> {
|
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
|
// Open -> Try to find the Song for the given file and then play it from all songs
|
||||||
is InternalPlayer.Action.Open -> {
|
is InternalPlayer.Action.Open -> {
|
||||||
|
@ -360,8 +363,8 @@ class PlaybackService :
|
||||||
playbackManager.play(
|
playbackManager.play(
|
||||||
song,
|
song,
|
||||||
null,
|
null,
|
||||||
settings.libSongSort,
|
musicSettings.songSort,
|
||||||
playbackManager.queue.isShuffled && settings.keepShuffle)
|
playbackManager.queue.isShuffled && playbackSettings.keepShuffle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -431,7 +434,7 @@ class PlaybackService :
|
||||||
// ACTION_HEADSET_PLUG will fire when this BroadcastReciever is initially attached,
|
// ACTION_HEADSET_PLUG will fire when this BroadcastReciever is initially attached,
|
||||||
// which would result in unexpected playback. Work around it by dropping the first
|
// which would result in unexpected playback. Work around it by dropping the first
|
||||||
// call to this function, which should come from that Intent.
|
// call to this function, which should come from that Intent.
|
||||||
if (settings.headsetAutoplay &&
|
if (playbackSettings.headsetAutoplay &&
|
||||||
playbackManager.queue.currentSong != null &&
|
playbackManager.queue.currentSong != null &&
|
||||||
initialHeadsetPlugEventHandled) {
|
initialHeadsetPlugEventHandled) {
|
||||||
logD("Device connected, resuming")
|
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.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -137,7 +137,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
override fun onRealClick(item: Music) {
|
override fun onRealClick(item: Music) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song ->
|
is Song ->
|
||||||
when (Settings(requireContext()).libPlaybackMode) {
|
when (PlaybackSettings.from(requireContext()).inListPlaybackMode) {
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
||||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(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.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.music.*
|
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.context
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -42,7 +44,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
class SearchViewModel(application: Application) :
|
class SearchViewModel(application: Application) :
|
||||||
AndroidViewModel(application), MusicStore.Listener {
|
AndroidViewModel(application), MusicStore.Listener {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settings = Settings(context)
|
private val settings = SearchSettings.from(application)
|
||||||
private var lastQuery: String? = null
|
private var lastQuery: String? = null
|
||||||
private var currentSearchJob: Job? = 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
|
* 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
|
* 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.Context
|
||||||
import android.content.SharedPreferences
|
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 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
|
* Abstract user configuration information. This interface has no functionality whatsoever. Concrete
|
||||||
* mutability is dependent on how they are used in app. Immutable members are often only modified by
|
* implementations should be preferred instead.
|
||||||
* the preferences view, while mutable members are modified elsewhere.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class Settings(private val context: Context) {
|
interface Settings {
|
||||||
private val inner = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
|
/** Migrate any settings fields from older versions into their new counterparts. */
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate any settings from an old version into their modern counterparts. This can cause data
|
|
||||||
* loss depending on the feasibility of a migration.
|
|
||||||
*/
|
|
||||||
fun migrate() {
|
fun migrate() {
|
||||||
if (inner.contains(OldKeys.KEY_ACCENT3)) {
|
throw NotImplementedError()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a [SharedPreferences.OnSharedPreferenceChangeListener] to monitor for settings updates.
|
* Add a [SharedPreferences.OnSharedPreferenceChangeListener] to monitor for settings updates.
|
||||||
* @param listener The [SharedPreferences.OnSharedPreferenceChangeListener] to add.
|
* @param listener The [SharedPreferences.OnSharedPreferenceChangeListener] to add.
|
||||||
*/
|
*/
|
||||||
fun addListener(listener: OnSharedPreferenceChangeListener) {
|
fun addListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||||
inner.registerOnSharedPreferenceChangeListener(listener)
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister a [SharedPreferences.OnSharedPreferenceChangeListener], preventing any further
|
* Unregister a [SharedPreferences.OnSharedPreferenceChangeListener], preventing any further
|
||||||
* settings updates from being sent to ti.t
|
* settings updates from being sent to ti.t
|
||||||
*/
|
*/
|
||||||
fun removeListener(listener: OnSharedPreferenceChangeListener) {
|
fun removeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||||
inner.unregisterOnSharedPreferenceChangeListener(listener)
|
throw NotImplementedError()
|
||||||
}
|
|
||||||
|
|
||||||
// --- 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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the configuration on how to handle particular directories in the music library.
|
* A framework-backed [Settings] implementation.
|
||||||
* @param musicDirs The new [MusicDirectories] configuration.
|
* @param context [Context] required.
|
||||||
*/
|
*/
|
||||||
fun setMusicDirs(musicDirs: MusicDirectories) {
|
abstract class Real(protected val context: Context) : Settings {
|
||||||
inner.edit {
|
protected val sharedPreferences: SharedPreferences =
|
||||||
putStringSet(
|
PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
|
||||||
context.getString(R.string.set_key_music_dirs),
|
|
||||||
musicDirs.dirs.map(Directory::toDocumentTreeUri).toSet())
|
override fun addListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||||
putBoolean(
|
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||||
context.getString(R.string.set_key_music_dirs_include), musicDirs.shouldInclude)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun removeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||||
* A string of characters representing the desired separator characters to denote multi-value
|
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
|
||||||
* 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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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.R
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.settings.SettingsFragmentDirections
|
import org.oxycblt.auxio.settings.SettingsFragmentDirections
|
||||||
|
import org.oxycblt.auxio.ui.UISettings
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
import org.oxycblt.auxio.util.isNight
|
import org.oxycblt.auxio.util.isNight
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -149,8 +149,6 @@ class PreferenceFragment : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupPreference(preference: Preference) {
|
private fun setupPreference(preference: Preference) {
|
||||||
val settings = Settings(requireContext())
|
|
||||||
|
|
||||||
if (!preference.isVisible) {
|
if (!preference.isVisible) {
|
||||||
// Nothing to do.
|
// Nothing to do.
|
||||||
return
|
return
|
||||||
|
@ -170,7 +168,7 @@ class PreferenceFragment : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getString(R.string.set_key_accent) -> {
|
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) -> {
|
getString(R.string.set_key_black_theme) -> {
|
||||||
preference.onPreferenceChangeListener =
|
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.R
|
||||||
import org.oxycblt.auxio.databinding.DialogAccentBinding
|
import org.oxycblt.auxio.databinding.DialogAccentBinding
|
||||||
import org.oxycblt.auxio.list.ClickableListListener
|
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.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
@ -44,7 +44,7 @@ class AccentCustomizeDialog :
|
||||||
builder
|
builder
|
||||||
.setTitle(R.string.set_accent)
|
.setTitle(R.string.set_accent)
|
||||||
.setPositiveButton(R.string.lbl_ok) { _, _ ->
|
.setPositiveButton(R.string.lbl_ok) { _, _ ->
|
||||||
val settings = Settings(requireContext())
|
val settings = UISettings.from(requireContext())
|
||||||
if (accentAdapter.selectedAccent == settings.accent) {
|
if (accentAdapter.selectedAccent == settings.accent) {
|
||||||
// Nothing to do.
|
// Nothing to do.
|
||||||
return@setPositiveButton
|
return@setPositiveButton
|
||||||
|
@ -65,7 +65,7 @@ class AccentCustomizeDialog :
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT))
|
Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT))
|
||||||
} else {
|
} 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.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.Queue
|
import org.oxycblt.auxio.playback.state.Queue
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
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.getDimenPixels
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
class WidgetComponent(private val context: Context) :
|
class WidgetComponent(private val context: Context) :
|
||||||
PlaybackStateManager.Listener, SharedPreferences.OnSharedPreferenceChangeListener {
|
PlaybackStateManager.Listener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
private val settings = Settings(context)
|
private val settings = UISettings.from(context)
|
||||||
private val widgetProvider = WidgetProvider()
|
private val widgetProvider = WidgetProvider()
|
||||||
private val provider = BitmapProvider(context)
|
private val provider = BitmapProvider(context)
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,6 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.playback.system.PlaybackService
|
import org.oxycblt.auxio.playback.system.PlaybackService
|
||||||
import org.oxycblt.auxio.settings.Settings
|
|
||||||
import org.oxycblt.auxio.util.*
|
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.
|
// 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.
|
// On API 31+, the bar should always be round in order to fit in with other widgets.
|
||||||
val background =
|
val background =
|
||||||
if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
if (useRoundedRemoteViews(context)) {
|
||||||
R.drawable.ui_widget_bar_round
|
R.drawable.ui_widget_bar_round
|
||||||
} else {
|
} else {
|
||||||
R.drawable.ui_widget_bar_system
|
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
|
// On API 31+, the background should always be round in order to fit in with other
|
||||||
// widgets.
|
// widgets.
|
||||||
val background =
|
val background =
|
||||||
if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
if (useRoundedRemoteViews(context)) {
|
||||||
R.drawable.ui_widget_bg_round
|
R.drawable.ui_widget_bg_round
|
||||||
} else {
|
} else {
|
||||||
R.drawable.ui_widget_bg_system
|
R.drawable.ui_widget_bg_system
|
||||||
|
|
|
@ -27,6 +27,7 @@ import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
import org.oxycblt.auxio.ui.UISettings
|
||||||
import org.oxycblt.auxio.util.isLandscape
|
import org.oxycblt.auxio.util.isLandscape
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
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
|
@Test
|
||||||
fun date_fromYearDate() {
|
fun date_fromYearDate() {
|
||||||
assertEquals("2016", Date.from(2016).toString())
|
assertEquals("2016-08-16", Date.from(20160816).toString())
|
||||||
assertEquals("2016", Date.from("2016").toString())
|
assertEquals("2016-08-16", Date.from("20160816").toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.oxycblt.auxio.music.FakeMusicSettings
|
||||||
|
|
||||||
class ParsingUtilTest {
|
class ParsingUtilTest {
|
||||||
@Test
|
@Test
|
||||||
fun parseMultiValue_single() {
|
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
|
@Test
|
||||||
fun parseMultiValue_many() {
|
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
|
@Test
|
||||||
fun parseMultiValue_several() {
|
fun parseMultiValue_several() {
|
||||||
assertEquals(
|
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
|
@Test
|
||||||
|
@ -105,37 +110,45 @@ class ParsingUtilTest {
|
||||||
fun parseId3v2Genre_multi() {
|
fun parseId3v2Genre_multi() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
||||||
listOf("Post-Rock", "Shoegaze", "Glitch").parseId3GenreNames(","))
|
listOf("Post-Rock", "Shoegaze", "Glitch")
|
||||||
|
.parseId3GenreNames(SeparatorMusicSettings(",")))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parseId3v2Genre_multiId3v1() {
|
fun parseId3v2Genre_multiId3v1() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
||||||
listOf("176", "178", "Glitch").parseId3GenreNames(","))
|
listOf("176", "178", "Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parseId3v2Genre_wackId3() {
|
fun parseId3v2Genre_wackId3() {
|
||||||
assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(","))
|
assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(SeparatorMusicSettings(",")))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parseId3v2Genre_singleId3v23() {
|
fun parseId3v2Genre_singleId3v23() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Post-Rock", "Shoegaze", "Remix", "Cover", "Glitch"),
|
listOf("Post-Rock", "Shoegaze", "Remix", "Cover", "Glitch"),
|
||||||
listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(","))
|
listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parseId3v2Genre_singleSeparated() {
|
fun parseId3v2Genre_singleSeparated() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
||||||
listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(","))
|
listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parsId3v2Genre_singleId3v1() {
|
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