accent: move back to ui
Move the accent module back into the ui module, where it's more consistent.
This commit is contained in:
parent
ac137d4cc8
commit
9d283fc6e4
110 changed files with 1159 additions and 1168 deletions
|
@ -74,14 +74,10 @@ class AuxioApp : Application(), ImageLoaderFactory {
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/** The ID of the "Shuffle All" shortcut. */
|
||||||
* The ID of the "Shuffle All" shortcut.
|
|
||||||
*/
|
|
||||||
const val SHORTCUT_SHUFFLE_ID = "shortcut_shuffle"
|
const val SHORTCUT_SHUFFLE_ID = "shortcut_shuffle"
|
||||||
|
|
||||||
/**
|
/** The [Intent] name for the "Shuffle All" shortcut. */
|
||||||
* The [Intent] name for the "Shuffle All" shortcut.
|
|
||||||
*/
|
|
||||||
const val INTENT_KEY_SHORTCUT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE_ALL"
|
const val INTENT_KEY_SHORTCUT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE_ALL"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
package org.oxycblt.auxio
|
package org.oxycblt.auxio
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A table containing all of the magic integer codes that the codebase has currently reserved.
|
* A table containing all of the magic integer codes that the codebase has currently reserved. May
|
||||||
* May be non-contiguous.
|
* be non-contiguous.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
object IntegerTable {
|
object IntegerTable {
|
||||||
|
|
|
@ -113,10 +113,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action]
|
* Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action] that can be used
|
||||||
* that can be used in the playback system.
|
* in the playback system.
|
||||||
* @param intent The (new) [Intent] given to this [MainActivity], or null if there
|
* @param intent The (new) [Intent] given to this [MainActivity], or null if there is no intent.
|
||||||
* is no intent.
|
|
||||||
* @return true If the analogous [InternalPlayer.Action] to the given [Intent] was started,
|
* @return true If the analogous [InternalPlayer.Action] to the given [Intent] was started,
|
||||||
* false otherwise.
|
* false otherwise.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -40,9 +40,9 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
|
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior
|
import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior
|
||||||
import org.oxycblt.auxio.shared.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -348,8 +348,8 @@ class MainFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [OnBackPressedCallback] that overrides the back button to first navigate out of
|
* A [OnBackPressedCallback] that overrides the back button to first navigate out of internal
|
||||||
* internal app components, such as the Bottom Sheets or Explore Navigation.
|
* app components, such as the Bottom Sheets or Explore Navigation.
|
||||||
*/
|
*/
|
||||||
private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) {
|
private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
|
@ -379,13 +379,13 @@ class MainFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force this instance to update whether it's enabled or not. If there are no app
|
* Force this instance to update whether it's enabled or not. If there are no app components
|
||||||
* components that the back button should close first, the instance is disabled and
|
* that the back button should close first, the instance is disabled and back navigation is
|
||||||
* back navigation is delegated to the system.
|
* delegated to the system.
|
||||||
*
|
*
|
||||||
* Normally, this callback would have just called the [MainActivity.onBackPressed]
|
* Normally, this callback would have just called the [MainActivity.onBackPressed] if there
|
||||||
* if there were no components to close, but that prevents adaptive back navigation
|
* were no components to close, but that prevents adaptive back navigation from working on
|
||||||
* from working on Android 14+, so we must do it this way.
|
* Android 14+, so we must do it this way.
|
||||||
*/
|
*/
|
||||||
fun invalidateEnabled() {
|
fun invalidateEnabled() {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
|
|
|
@ -174,7 +174,6 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
|
||||||
navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists)
|
navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun updateAlbum(album: Album?) {
|
private fun updateAlbum(album: Album?) {
|
||||||
if (album == null) {
|
if (album == null) {
|
||||||
// Album we were showing no longer exists.
|
// Album we were showing no longer exists.
|
||||||
|
|
|
@ -54,8 +54,7 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapte
|
||||||
// Information about what artist to display is initially within the navigation arguments
|
// Information about what artist to display is initially within the navigation arguments
|
||||||
// as a UID, as that is the only safe way to parcel an artist.
|
// as a UID, as that is the only safe way to parcel an artist.
|
||||||
private val args: ArtistDetailFragmentArgs by navArgs()
|
private val args: ArtistDetailFragmentArgs by navArgs()
|
||||||
private val detailAdapter =
|
private val detailAdapter = ArtistDetailAdapter(this)
|
||||||
ArtistDetailAdapter(this)
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
|
|
@ -31,7 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.shared.AuxioAppBarLayout
|
import org.oxycblt.auxio.ui.AuxioAppBarLayout
|
||||||
import org.oxycblt.auxio.util.getInteger
|
import org.oxycblt.auxio.util.getInteger
|
||||||
import org.oxycblt.auxio.util.lazyReflectedField
|
import org.oxycblt.auxio.util.lazyReflectedField
|
||||||
|
|
||||||
|
@ -75,7 +75,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
|
|
||||||
// The Toolbar's title view is actually hidden. To avoid having to create our own
|
// The Toolbar's title view is actually hidden. To avoid having to create our own
|
||||||
// title view, we just reflect into Toolbar and grab the hidden field.
|
// title view, we just reflect into Toolbar and grab the hidden field.
|
||||||
val newTitleView = (TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as TextView).apply {
|
val newTitleView =
|
||||||
|
(TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as TextView).apply {
|
||||||
// We can never properly initialize the title view's state before draw time,
|
// We can never properly initialize the title view's state before draw time,
|
||||||
// so we just set it's alpha to 0f to produce a less jarring initialization
|
// so we just set it's alpha to 0f to produce a less jarring initialization
|
||||||
// animation..
|
// animation..
|
||||||
|
@ -161,8 +162,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
// Title should be visible if we are no longer showing the top item
|
// Title should be visible if we are no longer showing the top item
|
||||||
// (i.e the header)
|
// (i.e the header)
|
||||||
appBarLayout.setTitleVisibility(
|
appBarLayout.setTitleVisibility(
|
||||||
(recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() > 0
|
(recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() > 0)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,9 +44,9 @@ import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [AndroidViewModel] that manages the Song, Album, Artist, and Genre detail views.
|
* [AndroidViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of
|
||||||
* Keeps track of the current item they are showing, sub-data to display, and configuration.
|
* the current item they are showing, sub-data to display, and configuration. Since this ViewModel
|
||||||
* Since this ViewModel requires a context, it must be instantiated [AndroidViewModel]'s Factory.
|
* requires a context, it must be instantiated [AndroidViewModel]'s Factory.
|
||||||
* @param application [Application] context required to initialize certain information.
|
* @param application [Application] context required to initialize certain information.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
@ -61,8 +61,8 @@ class DetailViewModel(application: Application) :
|
||||||
|
|
||||||
private val _currentSong = MutableStateFlow<DetailSong?>(null)
|
private val _currentSong = MutableStateFlow<DetailSong?>(null)
|
||||||
/**
|
/**
|
||||||
* The current [DetailSong] to display. Null if there is nothing to show.
|
* The current [DetailSong] to display. Null if there is nothing to show. TODO: De-couple Song
|
||||||
* TODO: De-couple Song and Properties?
|
* and Properties?
|
||||||
*/
|
*/
|
||||||
val currentSong: StateFlow<DetailSong?>
|
val currentSong: StateFlow<DetailSong?>
|
||||||
get() = _currentSong
|
get() = _currentSong
|
||||||
|
@ -392,8 +392,8 @@ class DetailViewModel(application: Application) :
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simpler mapping of [Album.Type] used for grouping and sorting songs.
|
* A simpler mapping of [Album.Type] used for grouping and sorting songs.
|
||||||
* @param headerTitleRes The title string resource to use for a header created
|
* @param headerTitleRes The title string resource to use for a header created out of an
|
||||||
* out of an instance of this enum.
|
* instance of this enum.
|
||||||
*/
|
*/
|
||||||
private enum class AlbumGrouping(@StringRes val headerTitleRes: Int) {
|
private enum class AlbumGrouping(@StringRes val headerTitleRes: Int) {
|
||||||
ALBUMS(R.string.lbl_albums),
|
ALBUMS(R.string.lbl_albums),
|
||||||
|
|
|
@ -55,8 +55,7 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapter
|
||||||
// Information about what genre to display is initially within the navigation arguments
|
// Information about what genre to display is initially within the navigation arguments
|
||||||
// as a UID, as that is the only safe way to parcel an genre.
|
// as a UID, as that is the only safe way to parcel an genre.
|
||||||
private val args: GenreDetailFragmentArgs by navArgs()
|
private val args: GenreDetailFragmentArgs by navArgs()
|
||||||
private val detailAdapter =
|
private val detailAdapter = GenreDetailAdapter(this)
|
||||||
GenreDetailAdapter(this)
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
|
@ -25,8 +25,8 @@ import com.google.android.material.textfield.TextInputEditText
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [TextInputEditText] that deliberately restricts all input except for selection. This will
|
* A [TextInputEditText] that deliberately restricts all input except for selection. This will work
|
||||||
* work just like a normal block of selectable/copyable text, but with nicer aesthetics.
|
* just like a normal block of selectable/copyable text, but with nicer aesthetics.
|
||||||
*
|
*
|
||||||
* Adapted from Material Files: https://github.com/zhanghai/MaterialFiles
|
* Adapted from Material Files: https://github.com/zhanghai/MaterialFiles
|
||||||
*
|
*
|
||||||
|
|
|
@ -27,7 +27,7 @@ import androidx.navigation.fragment.navArgs
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,8 @@ import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
|
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
|
||||||
import org.oxycblt.auxio.detail.DiscHeader
|
import org.oxycblt.auxio.detail.DiscHeader
|
||||||
import org.oxycblt.auxio.list.SelectableListListener
|
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
|
import org.oxycblt.auxio.list.SelectableListListener
|
||||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
|
|
@ -26,8 +26,8 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemParentBinding
|
import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||||
import org.oxycblt.auxio.list.SelectableListListener
|
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
|
import org.oxycblt.auxio.list.SelectableListListener
|
||||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
|
|
@ -26,9 +26,9 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
|
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
|
||||||
import org.oxycblt.auxio.detail.SortHeader
|
import org.oxycblt.auxio.detail.SortHeader
|
||||||
import org.oxycblt.auxio.list.SelectableListListener
|
|
||||||
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.list.SelectableListListener
|
||||||
import org.oxycblt.auxio.list.recycler.*
|
import org.oxycblt.auxio.list.recycler.*
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
|
|
@ -58,8 +58,8 @@ import org.oxycblt.auxio.music.MusicViewModel
|
||||||
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.music.system.Indexer
|
import org.oxycblt.auxio.music.system.Indexer
|
||||||
import org.oxycblt.auxio.shared.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,8 +100,7 @@ class HomeFragment :
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun getSelectionToolbar(binding: FragmentHomeBinding) =
|
override fun getSelectionToolbar(binding: FragmentHomeBinding) = binding.homeSelectionToolbar
|
||||||
binding.homeSelectionToolbar
|
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
|
||||||
super.onBindingCreated(binding, savedInstanceState)
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
@ -239,7 +238,8 @@ class HomeFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupPager(binding: FragmentHomeBinding) {
|
private fun setupPager(binding: FragmentHomeBinding) {
|
||||||
binding.homePager.adapter = HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner)
|
binding.homePager.adapter =
|
||||||
|
HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner)
|
||||||
|
|
||||||
val toolbarParams = binding.homeSelectionToolbar.layoutParams as AppBarLayout.LayoutParams
|
val toolbarParams = binding.homeSelectionToolbar.layoutParams as AppBarLayout.LayoutParams
|
||||||
if (homeModel.currentTabModes.size == 1) {
|
if (homeModel.currentTabModes.size == 1) {
|
||||||
|
@ -256,13 +256,17 @@ class HomeFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the mapping between the ViewPager and TabLayout.
|
// Set up the mapping between the ViewPager and TabLayout.
|
||||||
TabLayoutMediator(binding.homeTabs, binding.homePager,
|
TabLayoutMediator(
|
||||||
AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes)).attach()
|
binding.homeTabs,
|
||||||
|
binding.homePager,
|
||||||
|
AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes))
|
||||||
|
.attach()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCurrentTab(tabMode: MusicMode) {
|
private fun updateCurrentTab(tabMode: MusicMode) {
|
||||||
// Update the sort options to align with those allowed by the tab
|
// Update the sort options to align with those allowed by the tab
|
||||||
val isVisible: (Int) -> Boolean = when (tabMode) {
|
val isVisible: (Int) -> Boolean =
|
||||||
|
when (tabMode) {
|
||||||
// Disallow sorting by count for songs
|
// Disallow sorting by count for songs
|
||||||
MusicMode.SONGS -> { id -> id != R.id.option_sort_count }
|
MusicMode.SONGS -> { id -> id != R.id.option_sort_count }
|
||||||
// Disallow sorting by album for albums
|
// Disallow sorting by album for albums
|
||||||
|
@ -289,8 +293,9 @@ class HomeFragment :
|
||||||
for (option in sortMenu) {
|
for (option in sortMenu) {
|
||||||
// Check the ascending option and corresponding sort option to align with
|
// Check the ascending option and corresponding sort option to align with
|
||||||
// the current sort of the tab.
|
// the current sort of the tab.
|
||||||
option.isChecked = option.itemId == toHighlight.mode.itemId
|
option.isChecked =
|
||||||
|| (option.itemId == R.id.option_sort_asc && toHighlight.isAscending)
|
option.itemId == toHighlight.mode.itemId ||
|
||||||
|
(option.itemId == R.id.option_sort_asc && toHighlight.isAscending)
|
||||||
|
|
||||||
// Disable options that are not allowed by the isVisible lambda
|
// Disable options that are not allowed by the isVisible lambda
|
||||||
option.isVisible = isVisible(option.itemId)
|
option.isVisible = isVisible(option.itemId)
|
||||||
|
@ -454,8 +459,8 @@ class HomeFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the ID of the RecyclerView contained by [ViewPager2] tab represented with
|
* Get the ID of the RecyclerView contained by [ViewPager2] tab represented with the given
|
||||||
* the given [MusicMode].
|
* [MusicMode].
|
||||||
* @param tabMode The [MusicMode] of the tab.
|
* @param tabMode The [MusicMode] of the tab.
|
||||||
* @return The ID of the RecyclerView contained by the given tab.
|
* @return The ID of the RecyclerView contained by the given tab.
|
||||||
*/
|
*/
|
||||||
|
@ -478,8 +483,7 @@ class HomeFragment :
|
||||||
private val tabs: List<MusicMode>,
|
private val tabs: List<MusicMode>,
|
||||||
fragmentManager: FragmentManager,
|
fragmentManager: FragmentManager,
|
||||||
lifecycleOwner: LifecycleOwner
|
lifecycleOwner: LifecycleOwner
|
||||||
) :
|
) : FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle) {
|
||||||
FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle ) {
|
|
||||||
|
|
||||||
override fun getItemCount() = tabs.size
|
override fun getItemCount() = tabs.size
|
||||||
|
|
||||||
|
|
|
@ -44,60 +44,50 @@ class HomeViewModel(application: Application) :
|
||||||
private val settings = Settings(application, this)
|
private val settings = Settings(application, this)
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
val songLists: StateFlow<List<Song>>
|
val songLists: StateFlow<List<Song>>
|
||||||
get() = _songsList
|
get() = _songsList
|
||||||
|
|
||||||
private val _albumsLists = MutableStateFlow(listOf<Album>())
|
private val _albumsLists = MutableStateFlow(listOf<Album>())
|
||||||
/**
|
/** A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view. */
|
||||||
* A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view.
|
|
||||||
*/
|
|
||||||
val albumsList: StateFlow<List<Album>>
|
val albumsList: StateFlow<List<Album>>
|
||||||
get() = _albumsLists
|
get() = _albumsLists
|
||||||
|
|
||||||
private val _artistsList = MutableStateFlow(listOf<Artist>())
|
private val _artistsList = MutableStateFlow(listOf<Artist>())
|
||||||
/**
|
/**
|
||||||
* A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view.
|
* A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view. Note that
|
||||||
* Note that if "Hide collaborators" is on, this list will not include [Artist]s
|
* if "Hide collaborators" is on, this list will not include [Artist]s where
|
||||||
* where [Artist.isCollaborator] is true.
|
* [Artist.isCollaborator] is true.
|
||||||
*/
|
*/
|
||||||
val artistsList: MutableStateFlow<List<Artist>>
|
val artistsList: MutableStateFlow<List<Artist>>
|
||||||
get() = _artistsList
|
get() = _artistsList
|
||||||
|
|
||||||
private val _genresList = MutableStateFlow(listOf<Genre>())
|
private val _genresList = MutableStateFlow(listOf<Genre>())
|
||||||
/**
|
/** A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view. */
|
||||||
* A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view.
|
|
||||||
*/
|
|
||||||
val genresList: StateFlow<List<Genre>>
|
val genresList: StateFlow<List<Genre>>
|
||||||
get() = _genresList
|
get() = _genresList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of [MusicMode] corresponding to the current [Tab] configuration, excluding
|
* A list of [MusicMode] corresponding to the current [Tab] configuration, excluding invisible
|
||||||
* invisible [Tab]s.
|
* [Tab]s.
|
||||||
*/
|
*/
|
||||||
var currentTabModes: List<MusicMode> = makeTabModes()
|
var currentTabModes: List<MusicMode> = makeTabModes()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private val _currentTabMode = MutableStateFlow(currentTabModes[0])
|
private val _currentTabMode = MutableStateFlow(currentTabModes[0])
|
||||||
/**
|
/** The [MusicMode] of the currently shown [Tab]. */
|
||||||
* The [MusicMode] of the currently shown [Tab].
|
|
||||||
*/
|
|
||||||
val currentTabMode: StateFlow<MusicMode> = _currentTabMode
|
val currentTabMode: StateFlow<MusicMode> = _currentTabMode
|
||||||
|
|
||||||
private val _shouldRecreate = MutableStateFlow(false)
|
private val _shouldRecreate = MutableStateFlow(false)
|
||||||
/**
|
/**
|
||||||
* A marker to re-create all library tabs, usually initiated by a settings change.
|
* A marker to re-create all library tabs, usually initiated by a settings change. When this
|
||||||
* When this flag is true, all tabs (and their respective ViewPager2 fragments) will be
|
* flag is true, all tabs (and their respective ViewPager2 fragments) will be re-created from
|
||||||
* re-created from scratch.
|
* scratch.
|
||||||
*/
|
*/
|
||||||
val shouldRecreate: StateFlow<Boolean> = _shouldRecreate
|
val shouldRecreate: StateFlow<Boolean> = _shouldRecreate
|
||||||
|
|
||||||
private val _isFastScrolling = MutableStateFlow(false)
|
private val _isFastScrolling = MutableStateFlow(false)
|
||||||
/**
|
/** A marker for whether the user is fast-scrolling in the home view or not. */
|
||||||
* A marker for whether the user is fast-scrolling in the home view or not.
|
|
||||||
*/
|
|
||||||
val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
|
val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -136,7 +126,6 @@ class HomeViewModel(application: Application) :
|
||||||
currentTabModes = makeTabModes()
|
currentTabModes = makeTabModes()
|
||||||
_shouldRecreate.value = true
|
_shouldRecreate.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
context.getString(R.string.set_key_hide_collaborators) -> {
|
context.getString(R.string.set_key_hide_collaborators) -> {
|
||||||
// Changes in the hide collaborator setting will change the artist contents
|
// Changes in the hide collaborator setting will change the artist contents
|
||||||
// of the library, consider it a library update.
|
// of the library, consider it a library update.
|
||||||
|
@ -213,9 +202,8 @@ class HomeViewModel(application: Application) :
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a list of [MusicMode]s representing a simpler version of the [Tab] configuration.
|
* Create a list of [MusicMode]s representing a simpler version of the [Tab] configuration.
|
||||||
* @return A list of the [MusicMode]s for each visible [Tab] in the configuration,
|
* @return A list of the [MusicMode]s for each visible [Tab] in the configuration, ordered in
|
||||||
* ordered in the same way as the configuration.
|
* the same way as the configuration.
|
||||||
*/
|
*/
|
||||||
private fun makeTabModes() =
|
private fun makeTabModes() = settings.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
||||||
settings.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ package org.oxycblt.auxio.home.fastscroll
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.PointF
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
|
@ -72,22 +71,18 @@ class FastScrollRecyclerView
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
AuxioRecyclerView(context, attrs, defStyleAttr) {
|
AuxioRecyclerView(context, attrs, defStyleAttr) {
|
||||||
/**
|
/** An interface to provide text to use in the popup when fast-scrolling. */
|
||||||
* An interface to provide text to use in the popup when fast-scrolling.
|
|
||||||
*/
|
|
||||||
interface PopupProvider {
|
interface PopupProvider {
|
||||||
/**
|
/**
|
||||||
* Get text to use in the popup at the specified position.
|
* Get text to use in the popup at the specified position.
|
||||||
* @param pos The position in the list.
|
* @param pos The position in the list.
|
||||||
* @return A [String] to use in the popup. Null if there is no applicable text for
|
* @return A [String] to use in the popup. Null if there is no applicable text for the popup
|
||||||
* the popup at [pos].
|
* at [pos].
|
||||||
*/
|
*/
|
||||||
fun getPopup(pos: Int): String?
|
fun getPopup(pos: Int): String?
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A listener for fast scroller interactions. */
|
||||||
* A listener for fast scroller interactions.
|
|
||||||
*/
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
/**
|
/**
|
||||||
* Called when the fast scrolling state changes.
|
* Called when the fast scrolling state changes.
|
||||||
|
|
|
@ -46,7 +46,10 @@ import org.oxycblt.auxio.util.collectImmediately
|
||||||
* A [ListFragment] that shows a list of [Album]s.
|
* A [ListFragment] that shows a list of [Album]s.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class AlbumListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecyclerView.Listener, FastScrollRecyclerView.PopupProvider {
|
class AlbumListFragment :
|
||||||
|
ListFragment<FragmentHomeListBinding>(),
|
||||||
|
FastScrollRecyclerView.Listener,
|
||||||
|
FastScrollRecyclerView.PopupProvider {
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
private val albumAdapter = AlbumAdapter(this)
|
private val albumAdapter = AlbumAdapter(this)
|
||||||
// Save memory by re-using the same formatter and string builder when creating popup text
|
// Save memory by re-using the same formatter and string builder when creating popup text
|
||||||
|
|
|
@ -44,7 +44,10 @@ import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
* A [ListFragment] that shows a list of [Artist]s.
|
* A [ListFragment] that shows a list of [Artist]s.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class ArtistListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener {
|
class ArtistListFragment :
|
||||||
|
ListFragment<FragmentHomeListBinding>(),
|
||||||
|
FastScrollRecyclerView.PopupProvider,
|
||||||
|
FastScrollRecyclerView.Listener {
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
private val homeAdapter = ArtistAdapter(this)
|
private val homeAdapter = ArtistAdapter(this)
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,10 @@ import org.oxycblt.auxio.util.collectImmediately
|
||||||
* A [ListFragment] that shows a list of [Genre]s.
|
* A [ListFragment] that shows a list of [Genre]s.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class GenreListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener{
|
class GenreListFragment :
|
||||||
|
ListFragment<FragmentHomeListBinding>(),
|
||||||
|
FastScrollRecyclerView.PopupProvider,
|
||||||
|
FastScrollRecyclerView.Listener {
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
private val homeAdapter = GenreAdapter(this)
|
private val homeAdapter = GenreAdapter(this)
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,10 @@ import org.oxycblt.auxio.util.collectImmediately
|
||||||
* A [ListFragment] that shows a list of [Song]s.
|
* A [ListFragment] that shows a list of [Song]s.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class SongListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener {
|
class SongListFragment :
|
||||||
|
ListFragment<FragmentHomeListBinding>(),
|
||||||
|
FastScrollRecyclerView.PopupProvider,
|
||||||
|
FastScrollRecyclerView.Listener {
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
private val homeAdapter = SongAdapter(this)
|
private val homeAdapter = SongAdapter(this)
|
||||||
// Save memory by re-using the same formatter and string builder when creating popup text
|
// Save memory by re-using the same formatter and string builder when creating popup text
|
||||||
|
|
|
@ -21,7 +21,6 @@ import android.content.Context
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.home.tabs.Tab
|
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,7 @@ sealed class Tab(open val mode: MusicMode) {
|
||||||
data class Visible(override val mode: MusicMode) : Tab(mode)
|
data class Visible(override val mode: MusicMode) : Tab(mode)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A visible tab. This will be visible in the tab configuration view, but not in the
|
* A visible tab. This will be visible in the tab configuration view, but not in the home view.
|
||||||
* home view.
|
|
||||||
* @param mode The type of list in the home view this instance corresponds to.
|
* @param mode The type of list in the home view this instance corresponds to.
|
||||||
*/
|
*/
|
||||||
data class Invisible(override val mode: MusicMode) : Tab(mode)
|
data class Invisible(override val mode: MusicMode) : Tab(mode)
|
||||||
|
@ -58,9 +57,8 @@ sealed class Tab(open val mode: MusicMode) {
|
||||||
private const val SEQUENCE_LEN = 4
|
private const val SEQUENCE_LEN = 4
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default tab sequence, in integer form.
|
* The default tab sequence, in integer form. This represents a set of four visible tabs
|
||||||
* This represents a set of four visible tabs ordered as "Song", "Album", "Artist", and
|
* ordered as "Song", "Album", "Artist", and "Genre".
|
||||||
* "Genre".
|
|
||||||
*/
|
*/
|
||||||
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100
|
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100
|
||||||
|
|
||||||
|
|
|
@ -78,8 +78,8 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
|
||||||
/** A listener for interactions specific to tab configuration. */
|
/** A listener for interactions specific to tab configuration. */
|
||||||
interface Listener {
|
interface Listener {
|
||||||
/**
|
/**
|
||||||
* Called when a tab is clicked, requesting that the visibility should be inverted
|
* Called when a tab is clicked, requesting that the visibility should be inverted (i.e
|
||||||
* (i.e Visible -> Invisible and vice versa).
|
* Visible -> Invisible and vice versa).
|
||||||
* @param tabMode The [MusicMode] of the tab clicked.
|
* @param tabMode The [MusicMode] of the tab clicked.
|
||||||
*/
|
*/
|
||||||
fun onToggleVisibility(tabMode: MusicMode)
|
fun onToggleVisibility(tabMode: MusicMode)
|
||||||
|
|
|
@ -27,7 +27,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogTabsBinding
|
import org.oxycblt.auxio.databinding.DialogTabsBinding
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
|
|
@ -31,25 +31,26 @@ import org.oxycblt.auxio.music.Song
|
||||||
* A utility to provide bitmaps in a race-less manner.
|
* A utility to provide bitmaps in a race-less manner.
|
||||||
*
|
*
|
||||||
* When it comes to components that load images manually as [Bitmap] instances, queued
|
* When it comes to components that load images manually as [Bitmap] instances, queued
|
||||||
* [ImageRequest]s may cause a race condition that results in the incorrect image being
|
* [ImageRequest]s may cause a race condition that results in the incorrect image being drawn. This
|
||||||
* drawn. This utility resolves this by keeping track of the current request, and disposing
|
* utility resolves this by keeping track of the current request, and disposing it as soon as a new
|
||||||
* it as soon as a new request is queued or if another, competing request is newer.
|
* request is queued or if another, competing request is newer.
|
||||||
*
|
*
|
||||||
* @param context [Context] required to load images.
|
* @param context [Context] required to load images.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class BitmapProvider(private val context: Context) {
|
class BitmapProvider(private val context: Context) {
|
||||||
/** An extension of [Disposable] with an additional [Target] to deliver the final [Bitmap] to. */
|
/**
|
||||||
|
* An extension of [Disposable] with an additional [Target] to deliver the final [Bitmap] to.
|
||||||
|
*/
|
||||||
private data class Request(val disposable: Disposable, val callback: Target)
|
private data class Request(val disposable: Disposable, val callback: Target)
|
||||||
|
|
||||||
/** The target that will receive the requested [Bitmap]. */
|
/** The target that will receive the requested [Bitmap]. */
|
||||||
interface Target {
|
interface Target {
|
||||||
/**
|
/**
|
||||||
* Configure the [ImageRequest.Builder] to enable [Target]-specific configuration.
|
* Configure the [ImageRequest.Builder] to enable [Target]-specific configuration.
|
||||||
* @param builder The [ImageRequest.Builder] that will be used to request the
|
* @param builder The [ImageRequest.Builder] that will be used to request the desired
|
||||||
* desired [Bitmap].
|
* [Bitmap].
|
||||||
* @return The same [ImageRequest.Builder] in order to easily chain configuration
|
* @return The same [ImageRequest.Builder] in order to easily chain configuration methods.
|
||||||
* methods.
|
|
||||||
*/
|
*/
|
||||||
fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder = builder
|
fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder = builder
|
||||||
|
|
||||||
|
@ -80,7 +81,8 @@ class BitmapProvider(private val context: Context) {
|
||||||
currentRequest = null
|
currentRequest = null
|
||||||
|
|
||||||
val imageRequest =
|
val imageRequest =
|
||||||
target.onConfigRequest(
|
target
|
||||||
|
.onConfigRequest(
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data(song)
|
.data(song)
|
||||||
// Use ORIGINAL sizing, as we are not loading into any View-like component.
|
// Use ORIGINAL sizing, as we are not loading into any View-like component.
|
||||||
|
@ -110,9 +112,7 @@ class BitmapProvider(private val context: Context) {
|
||||||
currentRequest = Request(context.imageLoader.enqueue(imageRequest.build()), target)
|
currentRequest = Request(context.imageLoader.enqueue(imageRequest.build()), target)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Release this instance, cancelling any currently running operations. */
|
||||||
* Release this instance, cancelling any currently running operations.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun release() {
|
fun release() {
|
||||||
++currentHandle
|
++currentHandle
|
||||||
|
|
|
@ -39,8 +39,8 @@ import org.oxycblt.auxio.util.getDimenPixels
|
||||||
import org.oxycblt.auxio.util.getInteger
|
import org.oxycblt.auxio.util.getInteger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A super-charged [StyledImageView]. This class enables the following features in addition
|
* A super-charged [StyledImageView]. This class enables the following features in addition to
|
||||||
* to [StyledImageView]:
|
* [StyledImageView]:
|
||||||
* - A selection indicator
|
* - A selection indicator
|
||||||
* - An activation (playback) indicator
|
* - An activation (playback) indicator
|
||||||
* - Support for ONE custom view
|
* - Support for ONE custom view
|
||||||
|
@ -174,9 +174,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this view should be indicated to have ongoing playback or not. See
|
* Whether this view should be indicated to have ongoing playback or not. See
|
||||||
* PlaybackIndicatorView for more information on what occurs here.
|
* PlaybackIndicatorView for more information on what occurs here. Note: It's expected for this
|
||||||
* Note: It's expected for this view to already be marked as playing with setSelected
|
* view to already be marked as playing with setSelected (not the same thing) before this is set
|
||||||
* (not the same thing) before this is set to true.
|
* to true.
|
||||||
*/
|
*/
|
||||||
var isPlaying: Boolean
|
var isPlaying: Boolean
|
||||||
get() = playbackIndicatorView.isPlaying
|
get() = playbackIndicatorView.isPlaying
|
||||||
|
@ -214,13 +214,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
if (isActivated) {
|
if (isActivated) {
|
||||||
// View is "activated" (i.e marked as selected), so show the selection indicator.
|
// View is "activated" (i.e marked as selected), so show the selection indicator.
|
||||||
targetAlpha = 1f
|
targetAlpha = 1f
|
||||||
targetDuration =
|
targetDuration = context.getInteger(R.integer.anim_fade_enter_duration).toLong()
|
||||||
context.getInteger(R.integer.anim_fade_enter_duration).toLong()
|
|
||||||
} else {
|
} else {
|
||||||
// View is not "activated", hide the selection indicator.
|
// View is not "activated", hide the selection indicator.
|
||||||
targetAlpha = 0f
|
targetAlpha = 0f
|
||||||
targetDuration =
|
targetDuration = context.getInteger(R.integer.anim_fade_exit_duration).toLong()
|
||||||
context.getInteger(R.integer.anim_fade_exit_duration).toLong()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectionIndicatorView.alpha == targetAlpha) {
|
if (selectionIndicatorView.alpha == targetAlpha) {
|
||||||
|
|
|
@ -33,8 +33,8 @@ import org.oxycblt.auxio.util.getColorCompat
|
||||||
import org.oxycblt.auxio.util.getDrawableCompat
|
import org.oxycblt.auxio.util.getDrawableCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A view that displays an activation (i.e playback) indicator, with an accented styling and
|
* A view that displays an activation (i.e playback) indicator, with an accented styling and an
|
||||||
* an animated equalizer icon.
|
* animated equalizer icon.
|
||||||
*
|
*
|
||||||
* This is only meant for use with [ImageGroup]. Due to limitations with [AnimationDrawable]
|
* This is only meant for use with [ImageGroup]. Due to limitations with [AnimationDrawable]
|
||||||
* instances within custom views, this cannot be merged with [ImageGroup].
|
* instances within custom views, this cannot be merged with [ImageGroup].
|
||||||
|
@ -55,8 +55,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
private val settings = Settings(context)
|
private val settings = Settings(context)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The corner radius of this view. This allows the outer ImageGroup to apply it's
|
* The corner radius of this view. This allows the outer ImageGroup to apply it's corner radius
|
||||||
* corner radius to this view without any attribute hacks.
|
* to this view without any attribute hacks.
|
||||||
*/
|
*/
|
||||||
var cornerRadius = 0f
|
var cornerRadius = 0f
|
||||||
set(value) {
|
set(value) {
|
||||||
|
@ -71,8 +71,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this view should be indicated to have ongoing playback or not. If true,
|
* Whether this view should be indicated to have ongoing playback or not. If true, the animated
|
||||||
* the animated playing icon will be shown. If false, the static paused icon will be shown.
|
* playing icon will be shown. If false, the static paused icon will be shown.
|
||||||
*/
|
*/
|
||||||
var isPlaying: Boolean
|
var isPlaying: Boolean
|
||||||
get() = drawable == playingIndicatorDrawable
|
get() = drawable == playingIndicatorDrawable
|
||||||
|
|
|
@ -48,8 +48,8 @@ import org.oxycblt.auxio.util.getDrawableCompat
|
||||||
*
|
*
|
||||||
* - Tonal background
|
* - Tonal background
|
||||||
* - Rounded corners based on user preferences
|
* - Rounded corners based on user preferences
|
||||||
* - Built-in support for binding image data or using a static icon with the same
|
* - Built-in support for binding image data or using a static icon with the same styling as
|
||||||
* styling as placeholder drawables.
|
* placeholder drawables.
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
@ -116,8 +116,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
* Internally bind a [Music]'s image to this view.
|
* Internally bind a [Music]'s image to this view.
|
||||||
* @param music The music to find.
|
* @param music The music to find.
|
||||||
* @param errorRes The error drawable resource to use if the music cannot be loaded.
|
* @param errorRes The error drawable resource to use if the music cannot be loaded.
|
||||||
* @param descRes The content description string resource to use. The resource must have
|
* @param descRes The content description string resource to use. The resource must have one
|
||||||
* one field for the name of the [Music].
|
* field for the name of the [Music].
|
||||||
*/
|
*/
|
||||||
private fun bindImpl(music: Music, @DrawableRes errorRes: Int, @StringRes descRes: Int) {
|
private fun bindImpl(music: Music, @DrawableRes errorRes: Int, @StringRes descRes: Int) {
|
||||||
// Dispose of any previous image request and load a new image.
|
// Dispose of any previous image request and load a new image.
|
||||||
|
@ -132,8 +132,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Drawable] wrapper that re-styles the drawable to better align with the style
|
* A [Drawable] wrapper that re-styles the drawable to better align with the style of
|
||||||
* of [StyledImageView].
|
* [StyledImageView].
|
||||||
* @param context [Context] required for initialization.
|
* @param context [Context] required for initialization.
|
||||||
* @param inner The [Drawable] to wrap.
|
* @param inner The [Drawable] to wrap.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -52,8 +52,8 @@ class MusicKeyer : Keyer<Music> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic [Fetcher] for [Album] covers. Works with both [Album] and [Song].
|
* Generic [Fetcher] for [Album] covers. Works with both [Album] and [Song]. Use [SongFactory] or
|
||||||
* Use [SongFactory] or [AlbumFactory] for instantiation.
|
* [AlbumFactory] for instantiation.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class AlbumCoverFetcher
|
class AlbumCoverFetcher
|
||||||
|
@ -129,8 +129,8 @@ private constructor(
|
||||||
* Map at most N [T] items a collection into a collection of [R], ignoring [T] that cannot be
|
* Map at most N [T] items a collection into a collection of [R], ignoring [T] that cannot be
|
||||||
* transformed into [R].
|
* transformed into [R].
|
||||||
* @param n The maximum amount of items to map.
|
* @param n The maximum amount of items to map.
|
||||||
* @param transform The function that transforms data [T] from the original list into
|
* @param transform The function that transforms data [T] from the original list into data [R] in
|
||||||
* data [R] in the new list. Can return null if the [T] cannot be transformed into an [R].
|
* the new list. Can return null if the [T] cannot be transformed into an [R].
|
||||||
* @return A new list of at most N non-null [R] items.
|
* @return A new list of at most N non-null [R] items.
|
||||||
*/
|
*/
|
||||||
private inline fun <T : Any, R : Any> Collection<T>.mapAtMostNotNull(
|
private inline fun <T : Any, R : Any> Collection<T>.mapAtMostNotNull(
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.extractor
|
package org.oxycblt.auxio.image.extractor
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -7,6 +24,8 @@ import com.google.android.exoplayer2.MediaMetadata
|
||||||
import com.google.android.exoplayer2.MetadataRetriever
|
import com.google.android.exoplayer2.MetadataRetriever
|
||||||
import com.google.android.exoplayer2.metadata.flac.PictureFrame
|
import com.google.android.exoplayer2.metadata.flac.PictureFrame
|
||||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame
|
import com.google.android.exoplayer2.metadata.id3.ApicFrame
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
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
|
||||||
|
@ -14,8 +33,6 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.settings.Settings
|
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
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal utilities for loading album covers.
|
* Internal utilities for loading album covers.
|
||||||
|
@ -26,8 +43,8 @@ object Covers {
|
||||||
* Fetch an album cover, respecting the current cover configuration.
|
* Fetch an album cover, respecting the current cover configuration.
|
||||||
* @param context [Context] required to load the image.
|
* @param context [Context] required to load the image.
|
||||||
* @param album [Album] to load the cover from.
|
* @param album [Album] to load the cover from.
|
||||||
* @return An [InputStream] of image data if the cover loading was successful, null if the
|
* @return An [InputStream] of image data if the cover loading was successful, null if the cover
|
||||||
* cover 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)
|
val settings = Settings(context)
|
||||||
|
@ -45,8 +62,8 @@ object Covers {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load an [Album] cover directly from one of it's Song files. This attempts
|
* Load an [Album] cover directly from one of it's Song files. This attempts the following in
|
||||||
* the following in order:
|
* order:
|
||||||
* - [MediaMetadataRetriever], as it has the best support and speed.
|
* - [MediaMetadataRetriever], as it has the best support and speed.
|
||||||
* - ExoPlayer's [MetadataRetriever], as some devices (notably Samsung) can have broken
|
* - ExoPlayer's [MetadataRetriever], as some devices (notably Samsung) can have broken
|
||||||
* [MediaMetadataRetriever] implementations.
|
* [MediaMetadataRetriever] implementations.
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.extractor
|
package org.oxycblt.auxio.image.extractor
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -14,9 +31,9 @@ import coil.fetch.SourceResult
|
||||||
import coil.size.Dimension
|
import coil.size.Dimension
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import coil.size.pxOrElse
|
import coil.size.pxOrElse
|
||||||
|
import java.io.InputStream
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities for constructing Artist and Genre images.
|
* Utilities for constructing Artist and Genre images.
|
||||||
|
@ -24,8 +41,8 @@ import java.io.InputStream
|
||||||
*/
|
*/
|
||||||
object Images {
|
object Images {
|
||||||
/**
|
/**
|
||||||
* Create a mosaic image from the given image [InputStream]s.
|
* Create a mosaic image from the given image [InputStream]s. Derived from phonograph:
|
||||||
* Derived from phonograph: https://github.com/kabouzeid/Phonograph
|
* https://github.com/kabouzeid/Phonograph
|
||||||
* @param context [Context] required to generate the mosaic.
|
* @param context [Context] required to generate the mosaic.
|
||||||
* @param streams [InputStream]s of image data to create the mosaic out of.
|
* @param streams [InputStream]s of image data to create the mosaic out of.
|
||||||
* @param size [Size] of the Mosaic to generate.
|
* @param size [Size] of the Mosaic to generate.
|
||||||
|
|
|
@ -1,10 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.list
|
package org.oxycblt.auxio.list
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
|
||||||
/**
|
/** A marker for something that is a RecyclerView item. Has no functionality on it's own. */
|
||||||
* A marker for something that is a RecyclerView item. Has no functionality on it's own.
|
|
||||||
*/
|
|
||||||
interface Item
|
interface Item
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,8 +27,8 @@ import org.oxycblt.auxio.MainFragmentDirections
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
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.shared.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
|
||||||
|
@ -47,8 +47,8 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Selecta
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when [onClick] is called, but does not result in the item being selected. This
|
* Called when [onClick] is called, but does not result in the item being selected. This more or
|
||||||
* more or less corresponds to an [onClick] implementation in a non-[ListFragment].
|
* less corresponds to an [onClick] implementation in a non-[ListFragment].
|
||||||
* @param music The [Music] item that was clicked.
|
* @param music The [Music] item that was clicked.
|
||||||
*/
|
*/
|
||||||
abstract fun onRealClick(music: Music)
|
abstract fun onRealClick(music: Music)
|
||||||
|
@ -70,8 +70,8 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Selecta
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a menu in the context of a [Song]. This menu will be managed by the Fragment and
|
* Opens a menu in the context of a [Song]. This menu will be managed by the Fragment and closed
|
||||||
* closed when the view is destroyed. If a menu is already opened, this call is ignored.
|
* when the view is destroyed. If a menu is already opened, this call is ignored.
|
||||||
* @param anchor The [View] to anchor the menu to.
|
* @param anchor The [View] to anchor the menu to.
|
||||||
* @param menuRes The resource of the menu to load.
|
* @param menuRes The resource of the menu to load.
|
||||||
* @param song The [Song] to create the menu for.
|
* @param song The [Song] to create the menu for.
|
||||||
|
@ -223,8 +223,8 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Selecta
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a menu. This menu will be managed by the Fragment and closed when the view is
|
* Open a menu. This menu will be managed by the Fragment and closed when the view is destroyed.
|
||||||
* destroyed. If a menu is already opened, this call is ignored.
|
* If a menu is already opened, this call is ignored.
|
||||||
* @param anchor The [View] to anchor the menu to.
|
* @param anchor The [View] to anchor the menu to.
|
||||||
* @param menuRes The resource of the menu to load.
|
* @param menuRes The resource of the menu to load.
|
||||||
* @param block A block that is ran within [PopupMenu] that allows further configuration.
|
* @param block A block that is ran within [PopupMenu] that allows further configuration.
|
||||||
|
|
|
@ -1,14 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.list
|
package org.oxycblt.auxio.list
|
||||||
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic listener for list interactions.
|
* A basic listener for list interactions. TODO: Supply a ViewHolder on clicks (allows editable
|
||||||
* TODO: Supply a ViewHolder on clicks (allows editable lists to be standardized into a listener.)
|
* lists to be standardized into a listener.)
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
interface ClickableListListener {
|
interface ClickableListListener {
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.oxycblt.auxio.list.recycler
|
package org.oxycblt.auxio.list.recycler
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Rect
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
|
@ -56,8 +55,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
// Update the RecyclerView's padding such that the bottom insets are applied
|
// Update the RecyclerView's padding such that the bottom insets are applied
|
||||||
// while still preserving bottom padding.
|
// while still preserving bottom padding.
|
||||||
updatePadding(
|
updatePadding(bottom = initialPaddingBottom + insets.systemBarInsetsCompat.bottom)
|
||||||
bottom = initialPaddingBottom + insets.systemBarInsetsCompat.bottom)
|
|
||||||
return insets
|
return insets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,17 +100,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
lmm.findLastCompletelyVisibleItemPosition() == (lmm.itemCount - 1)
|
lmm.findLastCompletelyVisibleItemPosition() == (lmm.itemCount - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A [RecyclerView.ViewHolder] that implements dialog-specific fixes. */
|
||||||
* A [RecyclerView.ViewHolder] that implements dialog-specific fixes.
|
|
||||||
*/
|
|
||||||
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
||||||
init {
|
init {
|
||||||
// ViewHolders are not automatically full-width in dialogs, manually resize
|
// ViewHolders are not automatically full-width in dialogs, manually resize
|
||||||
// them to be as such.
|
// them to be as such.
|
||||||
root.layoutParams =
|
root.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||||
LayoutParams(
|
|
||||||
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [RecyclerView.Adapter] that supports indicating the playback status of a particular item.
|
* A [RecyclerView.Adapter] that supports indicating the playback status of a particular item.
|
||||||
|
@ -36,8 +35,7 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
|
||||||
private var isPlaying = false
|
private var isPlaying = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current list of the adapter. This is used to update items if the indicator
|
* The current list of the adapter. This is used to update items if the indicator state changes.
|
||||||
* state changes.
|
|
||||||
*/
|
*/
|
||||||
abstract val currentList: List<Item>
|
abstract val currentList: List<Item>
|
||||||
|
|
||||||
|
@ -106,15 +104,13 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A [RecyclerView.ViewHolder] that can display a playing indicator. */
|
||||||
* A [RecyclerView.ViewHolder] that can display a playing indicator.
|
|
||||||
*/
|
|
||||||
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
||||||
/**
|
/**
|
||||||
* Update the playing indicator within this [RecyclerView.ViewHolder].
|
* Update the playing indicator within this [RecyclerView.ViewHolder].
|
||||||
* @param isActive True if this item is playing, false otherwise.
|
* @param isActive True if this item is playing, false otherwise.
|
||||||
* @param isPlaying True if playback is ongoing, false if paused. If this
|
* @param isPlaying True if playback is ongoing, false if paused. If this is true,
|
||||||
* is true, [isActive] will also be true.
|
* [isActive] will also be true.
|
||||||
*/
|
*/
|
||||||
abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean)
|
abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean)
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,9 +68,7 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A [PlayingIndicatorAdapter.ViewHolder] that can display a selection indicator. */
|
||||||
* A [PlayingIndicatorAdapter.ViewHolder] that can display a selection indicator.
|
|
||||||
*/
|
|
||||||
abstract class ViewHolder(root: View) : PlayingIndicatorAdapter.ViewHolder(root) {
|
abstract class ViewHolder(root: View) : PlayingIndicatorAdapter.ViewHolder(root) {
|
||||||
/**
|
/**
|
||||||
* Update the selection indicator within this [PlayingIndicatorAdapter.ViewHolder].
|
* Update the selection indicator within this [PlayingIndicatorAdapter.ViewHolder].
|
||||||
|
|
|
@ -21,9 +21,8 @@ import androidx.recyclerview.widget.DiffUtil
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [DiffUtil.ItemCallback] that automatically implements the [areItemsTheSame] method.
|
* A [DiffUtil.ItemCallback] that automatically implements the [areItemsTheSame] method. Use this
|
||||||
* Use this whenever creating [DiffUtil.ItemCallback] implementations with an [Item]
|
* whenever creating [DiffUtil.ItemCallback] implementations with an [Item] subclass.
|
||||||
* subclass.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
||||||
|
|
|
@ -23,8 +23,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list differ that operates synchronously. This can help resolve some shortcomings with
|
* A list differ that operates synchronously. This can help resolve some shortcomings with
|
||||||
* AsyncListDiffer, at the cost of performance.
|
* AsyncListDiffer, at the cost of performance. Derived from Material Files:
|
||||||
* Derived from Material Files: https://github.com/zhanghai/MaterialFiles
|
* https://github.com/zhanghai/MaterialFiles
|
||||||
* @author Hai Zhang, Alexander Capehart (OxygenCobalt)
|
* @author Hai Zhang, Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class SyncListDiffer<T>(
|
class SyncListDiffer<T>(
|
||||||
|
@ -111,8 +111,8 @@ class SyncListDiffer<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only
|
* Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only use it
|
||||||
* use it if the changes are trivial.
|
* if the changes are trivial.
|
||||||
* @param newList The list to update to.
|
* @param newList The list to update to.
|
||||||
*/
|
*/
|
||||||
fun submitList(newList: List<T>) {
|
fun submitList(newList: List<T>) {
|
||||||
|
@ -125,8 +125,8 @@ class SyncListDiffer<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace this list with a new list. This is good for large diffs that are too slow to
|
* Replace this list with a new list. This is good for large diffs that are too slow to update
|
||||||
* update synchronously, but too chaotic to update asynchronously.
|
* synchronously, but too chaotic to update asynchronously.
|
||||||
* @param newList The list to update to.
|
* @param newList The list to update to.
|
||||||
*/
|
*/
|
||||||
fun replaceList(newList: List<T>) {
|
fun replaceList(newList: List<T>) {
|
||||||
|
|
|
@ -24,8 +24,8 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ItemHeaderBinding
|
import org.oxycblt.auxio.databinding.ItemHeaderBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemParentBinding
|
import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||||
import org.oxycblt.auxio.list.SelectableListListener
|
|
||||||
import org.oxycblt.auxio.list.Header
|
import org.oxycblt.auxio.list.Header
|
||||||
|
import org.oxycblt.auxio.list.SelectableListListener
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
|
|
@ -18,16 +18,14 @@
|
||||||
package org.oxycblt.auxio.list.selection
|
package org.oxycblt.auxio.list.selection
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,10 +38,10 @@ abstract class SelectionFragment<VB : ViewBinding> :
|
||||||
protected val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
protected val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [SelectionToolbarOverlay] of the concrete Fragment to be automatically managed
|
* Get the [SelectionToolbarOverlay] of the concrete Fragment to be automatically managed by
|
||||||
* by [SelectionFragment].
|
* [SelectionFragment].
|
||||||
* @return The [SelectionToolbarOverlay] of the concrete [SelectionFragment]'s [VB], or
|
* @return The [SelectionToolbarOverlay] of the concrete [SelectionFragment]'s [VB], or null if
|
||||||
* null if there is not one.
|
* there is not one.
|
||||||
*/
|
*/
|
||||||
open fun getSelectionToolbar(binding: VB): SelectionToolbarOverlay? = null
|
open fun getSelectionToolbar(binding: VB): SelectionToolbarOverlay? = null
|
||||||
|
|
||||||
|
|
|
@ -115,13 +115,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
if (selectionVisible) {
|
if (selectionVisible) {
|
||||||
targetInnerAlpha = 0f
|
targetInnerAlpha = 0f
|
||||||
targetSelectionAlpha = 1f
|
targetSelectionAlpha = 1f
|
||||||
targetDuration =
|
targetDuration = context.getInteger(R.integer.anim_fade_enter_duration).toLong()
|
||||||
context.getInteger(R.integer.anim_fade_enter_duration).toLong()
|
|
||||||
} else {
|
} else {
|
||||||
targetInnerAlpha = 1f
|
targetInnerAlpha = 1f
|
||||||
targetSelectionAlpha = 0f
|
targetSelectionAlpha = 0f
|
||||||
targetDuration =
|
targetDuration = context.getInteger(R.integer.anim_fade_exit_duration).toLong()
|
||||||
context.getInteger(R.integer.anim_fade_exit_duration).toLong()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (innerToolbar.alpha == targetInnerAlpha &&
|
if (innerToolbar.alpha == targetInnerAlpha &&
|
||||||
|
@ -154,8 +152,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the alpha of the inner and selection [MaterialToolbar]s.
|
* Update the alpha of the inner and selection [MaterialToolbar]s.
|
||||||
* @param innerAlpha The opacity of the inner [MaterialToolbar]. This will map to the
|
* @param innerAlpha The opacity of the inner [MaterialToolbar]. This will map to the inverse
|
||||||
* inverse opacity of the selection [MaterialToolbar].
|
* opacity of the selection [MaterialToolbar].
|
||||||
*/
|
*/
|
||||||
private fun setToolbarsAlpha(innerAlpha: Float) {
|
private fun setToolbarsAlpha(innerAlpha: Float) {
|
||||||
innerToolbar.apply {
|
innerToolbar.apply {
|
||||||
|
|
|
@ -21,7 +21,6 @@ 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.util.logD
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [ViewModel] that manages the current selection.
|
* A [ViewModel] that manages the current selection.
|
||||||
|
@ -31,10 +30,7 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
private val _selected = MutableStateFlow(listOf<Music>())
|
private val _selected = MutableStateFlow(listOf<Music>())
|
||||||
/**
|
/** the currently selected items. These are ordered in earliest selected and latest selected. */
|
||||||
* the currently selected items. These are ordered in earliest selected
|
|
||||||
* and latest selected.
|
|
||||||
*/
|
|
||||||
val selected: StateFlow<List<Music>>
|
val selected: StateFlow<List<Music>>
|
||||||
get() = _selected
|
get() = _selected
|
||||||
|
|
||||||
|
@ -49,7 +45,8 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback {
|
||||||
|
|
||||||
// Sanitize the selection to remove items that no longer exist and thus
|
// Sanitize the selection to remove items that no longer exist and thus
|
||||||
// won't appear in any list.
|
// won't appear in any list.
|
||||||
_selected.value = _selected.value.mapNotNull {
|
_selected.value =
|
||||||
|
_selected.value.mapNotNull {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Song -> library.sanitize(it)
|
is Song -> library.sanitize(it)
|
||||||
is Album -> library.sanitize(it)
|
is Album -> library.sanitize(it)
|
||||||
|
@ -65,8 +62,8 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select a new [Music] item. If this item is already within the selected items, the item will be
|
* Select a new [Music] item. If this item is already within the selected items, the item will
|
||||||
* removed. Otherwise, it will be added.
|
* be removed. Otherwise, it will be added.
|
||||||
* @param music The [Music] item to select.
|
* @param music The [Music] item to select.
|
||||||
*/
|
*/
|
||||||
fun select(music: Music) {
|
fun select(music: Music) {
|
||||||
|
@ -81,6 +78,5 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback {
|
||||||
* Consume the current selection. This will clear any items that were selected prior.
|
* Consume the current selection. This will clear any items that were selected prior.
|
||||||
* @return The list of selected items before it was cleared.
|
* @return The list of selected items before it was cleared.
|
||||||
*/
|
*/
|
||||||
fun consume() =
|
fun consume() = _selected.value.also { _selected.value = listOf() }
|
||||||
_selected.value.also { _selected.value = listOf() }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// We use a special naming convention for internal fields, disable the lints that check for that.
|
|
||||||
@file:Suppress("PropertyName", "FunctionName")
|
@file:Suppress("PropertyName", "FunctionName")
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
@ -25,6 +24,8 @@ import android.os.Parcelable
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.text.CollationKey
|
import java.text.CollationKey
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
|
@ -39,14 +40,12 @@ import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.inRangeOrNull
|
import org.oxycblt.auxio.util.inRangeOrNull
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
|
|
||||||
// --- MUSIC MODELS ---
|
// --- MUSIC MODELS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract music data. This contains universal information about all concrete music implementations,
|
* Abstract music data. This contains universal information about all concrete music
|
||||||
* such as identification information and names.
|
* implementations, such as identification information and names.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
sealed class Music : Item {
|
sealed class Music : Item {
|
||||||
|
@ -66,34 +65,33 @@ sealed class Music : Item {
|
||||||
* Returns a name suitable for use in the app UI. This should be favored over [rawName] in
|
* Returns a name suitable for use in the app UI. This should be favored over [rawName] in
|
||||||
* nearly all cases.
|
* nearly all cases.
|
||||||
* @param context [Context] required to obtain placeholder text or formatting information.
|
* @param context [Context] required to obtain placeholder text or formatting information.
|
||||||
* @return A human-readable string representing the name of this music. In the case that
|
* @return A human-readable string representing the name of this music. In the case that the
|
||||||
* the item does not have a name, an analogous "Unknown X" name is returned.
|
* item does not have a name, an analogous "Unknown X" name is returned.
|
||||||
*/
|
*/
|
||||||
abstract fun resolveName(context: Context): String
|
abstract fun resolveName(context: Context): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The raw sort name of this item as it was extracted from the file-system. This can be used
|
* The raw sort name of this item as it was extracted from the file-system. This can be used not
|
||||||
* not only when sorting music, but also trying to locate music based on a fuzzy search by
|
* only when sorting music, but also trying to locate music based on a fuzzy search by the user.
|
||||||
* the user. Will be null if the item has no known sort name.
|
* Will be null if the item has no known sort name.
|
||||||
*/
|
*/
|
||||||
abstract val rawSortName: String?
|
abstract val rawSortName: String?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [CollationKey] derived from [rawName] and [rawSortName] that can be used to sort items
|
* A [CollationKey] derived from [rawName] and [rawSortName] that can be used to sort items in a
|
||||||
* in a semantically-correct manner. Will be null if the item has no name.
|
* semantically-correct manner. Will be null if the item has no name.
|
||||||
*
|
*
|
||||||
* The key will have the following attributes:
|
* The key will have the following attributes:
|
||||||
* - If [rawSortName] is present, this key will be derived from it. Otherwise [rawName]
|
* - If [rawSortName] is present, this key will be derived from it. Otherwise [rawName] is used.
|
||||||
* is used.
|
|
||||||
* - If the string begins with an article, such as "the", it will be stripped, as is usually
|
* - If the string begins with an article, such as "the", it will be stripped, as is usually
|
||||||
* convention for sorting media. This is not internationalized.
|
* convention for sorting media. This is not internationalized.
|
||||||
*/
|
*/
|
||||||
abstract val collationKey: CollationKey?
|
abstract val collationKey: CollationKey?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finalize this item once the music library has been fully constructed. This is where
|
* Finalize this item once the music library has been fully constructed. This is where any final
|
||||||
* any final ordering or sanity checking should occur.
|
* ordering or sanity checking should occur. **This function is internal to the music package.
|
||||||
* **This function is internal to the music package. Do not use it elsewhere.**
|
* Do not use it elsewhere.**
|
||||||
*/
|
*/
|
||||||
abstract fun _finalize()
|
abstract fun _finalize()
|
||||||
|
|
||||||
|
@ -128,20 +126,20 @@ sealed class Music : Item {
|
||||||
* A unique identifier for a piece of music.
|
* A unique identifier for a piece of music.
|
||||||
*
|
*
|
||||||
* [UID] enables a much cheaper and more reliable form of differentiating music, derived from
|
* [UID] enables a much cheaper and more reliable form of differentiating music, derived from
|
||||||
* either a hash of meaningful metadata or the MusicBrainz ID spec. Using this enables
|
* either a hash of meaningful metadata or the MusicBrainz ID spec. Using this enables several
|
||||||
* several improvements to music management in this app, including:
|
* improvements to music management in this app, including:
|
||||||
*
|
*
|
||||||
* - Proper differentiation of identical music. It's common for large, well-tagged libraries
|
* - Proper differentiation of identical music. It's common for large, well-tagged libraries to
|
||||||
* to have functionally duplicate items that are differentiated with MusicBrainz IDs, and so
|
* have functionally duplicate items that are differentiated with MusicBrainz IDs, and so [UID]
|
||||||
* [UID] allows us to properly differentiate between these in the app.
|
* allows us to properly differentiate between these in the app.
|
||||||
* - Better music persistence between restarts. Whereas directly storing song names would be
|
* - Better music persistence between restarts. Whereas directly storing song names would be
|
||||||
* prone to collisions, and storing MediaStore IDs would drift rapidly as the music library
|
* prone to collisions, and storing MediaStore IDs would drift rapidly as the music library
|
||||||
* changes, [UID] enables a much stronger form of persistence given it's unique link to a
|
* changes, [UID] enables a much stronger form of persistence given it's unique link to a
|
||||||
* specific files metadata configuration, which is unlikely to collide with another item
|
* specific files metadata configuration, which is unlikely to collide with another item or
|
||||||
* or drift as the music library changes.
|
* drift as the music library changes.
|
||||||
*
|
*
|
||||||
* Note: Generally try to use [UID] as a black box that can only be read, written, and
|
* Note: Generally try to use [UID] as a black box that can only be read, written, and compared.
|
||||||
* compared. It will not be fun if you try to manipulate it in any other manner.
|
* It will not be fun if you try to manipulate it in any other manner.
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
@ -180,11 +178,11 @@ sealed class Music : Item {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Creates an auxio-style [UID] with a [UUID] composed of a hash of the
|
* Creates an auxio-style [UID] with a [UUID] composed of a hash of the non-subjective,
|
||||||
* non-subjective, unlikely-to-change metadata of the music.
|
* unlikely-to-change metadata of the music.
|
||||||
* @param mode The analogous [MusicMode] of the item that created this [UID].
|
* @param mode The analogous [MusicMode] of the item that created this [UID].
|
||||||
* @param updates Block to update the [MessageDigest] hash with the metadata of
|
* @param updates Block to update the [MessageDigest] hash with the metadata of the
|
||||||
* the item. Make sure the metadata hashed semantically aligns with the format
|
* item. Make sure the metadata hashed semantically aligns with the format
|
||||||
* specification.
|
* specification.
|
||||||
* @return A new auxio-style [UID].
|
* @return A new auxio-style [UID].
|
||||||
*/
|
*/
|
||||||
|
@ -192,7 +190,8 @@ sealed class Music : Item {
|
||||||
// Auxio hashes consist of the MD5 hash of the non-subjective, consistent
|
// Auxio hashes consist of the MD5 hash of the non-subjective, consistent
|
||||||
// tags in a music item. For easier use with MusicBrainz IDs, we transform
|
// tags in a music item. For easier use with MusicBrainz IDs, we transform
|
||||||
// this into a UUID too.
|
// this into a UUID too.
|
||||||
val uuid = MessageDigest.getInstance("MD5").run {
|
val uuid =
|
||||||
|
MessageDigest.getInstance("MD5").run {
|
||||||
updates()
|
updates()
|
||||||
digest().toUuid()
|
digest().toUuid()
|
||||||
}
|
}
|
||||||
|
@ -235,7 +234,8 @@ sealed class Music : Item {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val mode = MusicMode.fromIntCode(ids[0].toIntOrNull(16) ?: return null) ?: return null
|
val mode =
|
||||||
|
MusicMode.fromIntCode(ids[0].toIntOrNull(16) ?: return null) ?: return null
|
||||||
val uuid = ids[1].toUuidOrNull() ?: return null
|
val uuid = ids[1].toUuidOrNull() ?: return null
|
||||||
|
|
||||||
return UID(format, mode, uuid)
|
return UID(format, mode, uuid)
|
||||||
|
@ -254,9 +254,7 @@ sealed class Music : Item {
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
sealed class MusicParent : Music() {
|
sealed class MusicParent : Music() {
|
||||||
/**
|
/** The [Song]s in this this group. */
|
||||||
* The [Song]s in this this group.
|
|
||||||
*/
|
|
||||||
abstract val songs: List<Song>
|
abstract val songs: List<Song>
|
||||||
|
|
||||||
// Note: Append song contents to MusicParent equality so that Groups with
|
// Note: Append song contents to MusicParent equality so that Groups with
|
||||||
|
@ -310,14 +308,14 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
val date = raw.date
|
val date = raw.date
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URI to the audio file that this instance was created from. This can be used to
|
* The URI to the audio file that this instance was created from. This can be used to access the
|
||||||
* access the audio file in a way that is scoped-storage-safe.
|
* audio file in a way that is scoped-storage-safe.
|
||||||
*/
|
*/
|
||||||
val uri = requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri()
|
val uri = requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [Path] to this audio file. This is only intended for display, [uri] should be
|
* The [Path] to this audio file. This is only intended for display, [uri] should be favored
|
||||||
* favored instead for accessing the audio file.
|
* instead for accessing the audio file.
|
||||||
*/
|
*/
|
||||||
val path =
|
val path =
|
||||||
Path(
|
Path(
|
||||||
|
@ -341,8 +339,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
|
|
||||||
private var _album: Album? = null
|
private var _album: Album? = null
|
||||||
/**
|
/**
|
||||||
* The parent [Album]. If the metadata did not specify an album, it's parent directory is
|
* The parent [Album]. If the metadata did not specify an album, it's parent directory is used
|
||||||
* used instead.
|
* instead.
|
||||||
*/
|
*/
|
||||||
val album: Album
|
val album: Album
|
||||||
get() = unlikelyToBeNull(_album)
|
get() = unlikelyToBeNull(_album)
|
||||||
|
@ -371,23 +369,23 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
|
|
||||||
private val _artists = mutableListOf<Artist>()
|
private val _artists = mutableListOf<Artist>()
|
||||||
/**
|
/**
|
||||||
* The parent [Artist]s of this [Song]. Is often one, but there can be multiple if more
|
* The parent [Artist]s of this [Song]. Is often one, but there can be multiple if more than one
|
||||||
* than one [Artist] name was specified in the metadata. Unliked [Album], artists are
|
* [Artist] name was specified in the metadata. Unliked [Album], artists are prioritized for
|
||||||
* prioritized for this field.
|
* this field.
|
||||||
*/
|
*/
|
||||||
val artists: List<Artist>
|
val artists: List<Artist>
|
||||||
get() = _artists
|
get() = _artists
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves one or more [Artist]s into a single piece of human-readable names.
|
* Resolves one or more [Artist]s into a single piece of human-readable names.
|
||||||
* @param context [Context] required for [resolveName].
|
* @param context [Context] required for [resolveName]. TODO Internationalize the list
|
||||||
* TODO Internationalize the list formatter.
|
* formatter.
|
||||||
*/
|
*/
|
||||||
fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
|
fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the [Artist] *display* of this [Song] and another [Song] are equal. This
|
* Checks if the [Artist] *display* of this [Song] and another [Song] are equal. This will only
|
||||||
* will only compare surface-level names, and not [Music.UID]s.
|
* compare surface-level names, and not [Music.UID]s.
|
||||||
* @param other The [Song] to compare to.
|
* @param other The [Song] to compare to.
|
||||||
* @return True if the [Artist] displays are equal, false otherwise
|
* @return True if the [Artist] displays are equal, false otherwise
|
||||||
*/
|
*/
|
||||||
|
@ -405,8 +403,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
|
|
||||||
private val _genres = mutableListOf<Genre>()
|
private val _genres = mutableListOf<Genre>()
|
||||||
/**
|
/**
|
||||||
* The parent [Genre]s of this [Song]. Is often one, but there can be multiple if more
|
* The parent [Genre]s of this [Song]. Is often one, but there can be multiple if more than one
|
||||||
* than one [Genre] name was specified in the metadata.
|
* [Genre] name was specified in the metadata.
|
||||||
*/
|
*/
|
||||||
val genres: List<Genre>
|
val genres: List<Genre>
|
||||||
get() = _genres
|
get() = _genres
|
||||||
|
@ -420,9 +418,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
// --- INTERNAL FIELDS ---
|
// --- INTERNAL FIELDS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [Album.Raw] instances collated by the [Song]. This can be used to group [Song]s into
|
* The [Album.Raw] instances collated by the [Song]. This can be used to group [Song]s into an
|
||||||
* an [Album].
|
* [Album]. **This is only meant for use within the music package.**
|
||||||
* **This is only meant for use within the music package.**
|
|
||||||
*/
|
*/
|
||||||
val _rawAlbum =
|
val _rawAlbum =
|
||||||
Album.Raw(
|
Album.Raw(
|
||||||
|
@ -435,19 +432,17 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
|
rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [Artist.Raw] instances collated by the [Song]. The artists of the song take
|
* The [Artist.Raw] instances collated by the [Song]. The artists of the song take priority,
|
||||||
* priority, followed by the album artists. If there are no artists, this field will
|
* followed by the album artists. If there are no artists, this field will be a single "unknown"
|
||||||
* be a single "unknown" [Artist.Raw]. This can be used to group up [Song]s into
|
* [Artist.Raw]. This can be used to group up [Song]s into an [Artist]. **This is only meant for
|
||||||
* an [Artist].
|
* use within the music package.**
|
||||||
* **This is only meant for use within the music package.**
|
|
||||||
*/
|
*/
|
||||||
val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(Artist.Raw()) }
|
val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(Artist.Raw()) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [Genre.Raw] instances collated by the [Song]. This can be used to group up
|
* The [Genre.Raw] instances collated by the [Song]. This can be used to group up [Song]s into a
|
||||||
* [Song]s into a [Genre]. ID3v2 Genre names are automatically converted to their
|
* [Genre]. ID3v2 Genre names are automatically converted to their resolved names. **This is
|
||||||
* resolved names.
|
* only meant for use within the music package.**
|
||||||
* **This is only meant for use within the music package.**
|
|
||||||
*/
|
*/
|
||||||
val _rawGenres =
|
val _rawGenres =
|
||||||
raw.genreNames
|
raw.genreNames
|
||||||
|
@ -457,8 +452,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links this [Song] with a parent [Album].
|
* Links this [Song] with a parent [Album].
|
||||||
* @param album The parent [Album] to link to.
|
* @param album The parent [Album] to link to. **This is only meant for use within the music
|
||||||
* **This is only meant for use within the music package.**
|
* package.**
|
||||||
*/
|
*/
|
||||||
fun _link(album: Album) {
|
fun _link(album: Album) {
|
||||||
_album = album
|
_album = album
|
||||||
|
@ -466,8 +461,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links this [Song] with a parent [Artist].
|
* Links this [Song] with a parent [Artist].
|
||||||
* @param artist The parent [Artist] to link to.
|
* @param artist The parent [Artist] to link to. **This is only meant for use within the music
|
||||||
* **This is only meant for use within the music package.**
|
* package.**
|
||||||
*/
|
*/
|
||||||
fun _link(artist: Artist) {
|
fun _link(artist: Artist) {
|
||||||
_artists.add(artist)
|
_artists.add(artist)
|
||||||
|
@ -475,8 +470,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links this [Song] with a parent [Genre].
|
* Links this [Song] with a parent [Genre].
|
||||||
* @param genre The parent [Genre] to link to.
|
* @param genre The parent [Genre] to link to. **This is only meant for use within the music
|
||||||
* **This is only meant for use within the music package.**
|
* package.**
|
||||||
*/
|
*/
|
||||||
fun _link(genre: Genre) {
|
fun _link(genre: Genre) {
|
||||||
_genres.add(genre)
|
_genres.add(genre)
|
||||||
|
@ -508,14 +503,14 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Raw information about a [Song] obtained from the filesystem/Extractor instances.
|
* Raw information about a [Song] obtained from the filesystem/Extractor instances. **This is
|
||||||
* **This is only meant for use within the music package.**
|
* only meant for use within the music package.**
|
||||||
*/
|
*/
|
||||||
class Raw
|
class Raw
|
||||||
constructor(
|
constructor(
|
||||||
/**
|
/**
|
||||||
* The ID of the [Song]'s audio file, obtained from MediaStore. Note that this
|
* The ID of the [Song]'s audio file, obtained from MediaStore. Note that this ID is highly
|
||||||
* ID is highly unstable and should only be used for accessing the audio file.
|
* unstable and should only be used for accessing the audio file.
|
||||||
*/
|
*/
|
||||||
var mediaStoreId: Long? = null,
|
var mediaStoreId: Long? = null,
|
||||||
/** @see Song.dateAdded */
|
/** @see Song.dateAdded */
|
||||||
|
@ -598,23 +593,20 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
override fun resolveName(context: Context) = rawName
|
override fun resolveName(context: Context) = rawName
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The earliest [Date] this album was released.
|
* The earliest [Date] this album was released. Will be null if no valid date was present in the
|
||||||
* Will be null if no valid date was present in the metadata of any [Song].
|
* metadata of any [Song]. TODO: Date ranges?
|
||||||
* TODO: Date ranges?
|
|
||||||
*/
|
*/
|
||||||
val date: Date?
|
val date: Date?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [Type] of this album, signifying the type of release it actually is.
|
* The [Type] of this album, signifying the type of release it actually is. Defaults to
|
||||||
* Defaults to [Type.Album].
|
* [Type.Album].
|
||||||
*/
|
*/
|
||||||
|
|
||||||
val type = raw.type ?: Type.Album(null)
|
val type = raw.type ?: Type.Album(null)
|
||||||
/**
|
/**
|
||||||
* The URI to a MediaStore-provided album cover. These images will be fast to load, but
|
* The URI to a MediaStore-provided album cover. These images will be fast to load, but at the
|
||||||
* at the cost of image quality.
|
* cost of image quality.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
val coverUri = raw.mediaStoreId.toCoverUri()
|
val coverUri = raw.mediaStoreId.toCoverUri()
|
||||||
|
|
||||||
/** The duration of all songs in the album, in milliseconds. */
|
/** The duration of all songs in the album, in milliseconds. */
|
||||||
|
@ -655,9 +647,9 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
|
|
||||||
private val _artists = mutableListOf<Artist>()
|
private val _artists = mutableListOf<Artist>()
|
||||||
/**
|
/**
|
||||||
* The parent [Artist]s of this [Album]. Is often one, but there can be multiple if more
|
* The parent [Artist]s of this [Album]. Is often one, but there can be multiple if more than
|
||||||
* than one [Artist] name was specified in the metadata of the [Song]'s. Unlike [Song],
|
* one [Artist] name was specified in the metadata of the [Song]'s. Unlike [Song], album artists
|
||||||
* album artists are prioritized for this field.
|
* are prioritized for this field.
|
||||||
*/
|
*/
|
||||||
val artists: List<Artist>
|
val artists: List<Artist>
|
||||||
get() = _artists
|
get() = _artists
|
||||||
|
@ -669,8 +661,8 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
|
fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the [Artist] *display* of this [Album] and another [Album] are equal. This
|
* Checks if the [Artist] *display* of this [Album] and another [Album] are equal. This will
|
||||||
* will only compare surface-level names, and not [Music.UID]s.
|
* only compare surface-level names, and not [Music.UID]s.
|
||||||
* @param other The [Album] to compare to.
|
* @param other The [Album] to compare to.
|
||||||
* @return True if the [Artist] displays are equal, false otherwise
|
* @return True if the [Artist] displays are equal, false otherwise
|
||||||
*/
|
*/
|
||||||
|
@ -690,17 +682,16 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [Artist.Raw] instances collated by the [Album]. The album artists of the song take
|
* The [Artist.Raw] instances collated by the [Album]. The album artists of the song take
|
||||||
* priority, followed by the artists. If there are no artists, this field will
|
* priority, followed by the artists. If there are no artists, this field will be a single
|
||||||
* be a single "unknown" [Artist.Raw]. This can be used to group up [Album]s into
|
* "unknown" [Artist.Raw]. This can be used to group up [Album]s into an [Artist]. **This is
|
||||||
* an [Artist].
|
* only meant for use within the music package.**
|
||||||
* **This is only meant for use within the music package.**
|
|
||||||
*/
|
*/
|
||||||
val _rawArtists = raw.rawArtists
|
val _rawArtists = raw.rawArtists
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links this [Album] with a parent [Artist].
|
* Links this [Album] with a parent [Artist].
|
||||||
* @param artist The parent [Artist] to link to.
|
* @param artist The parent [Artist] to link to. **This is only meant for use within the music
|
||||||
* **This is only meant for use within the music package.**
|
* package.**
|
||||||
*/
|
*/
|
||||||
fun _link(artist: Artist) {
|
fun _link(artist: Artist) {
|
||||||
_artists.add(artist)
|
_artists.add(artist)
|
||||||
|
@ -722,8 +713,8 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
/**
|
/**
|
||||||
* The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc.
|
* The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc.
|
||||||
*
|
*
|
||||||
* This class is derived from the MusicBrainz Release Group Type specification. It can
|
* This class is derived from the MusicBrainz Release Group Type specification. It can be found
|
||||||
* be found at: https://musicbrainz.org/doc/Release_Group/Type
|
* at: https://musicbrainz.org/doc/Release_Group/Type
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
sealed class Type {
|
sealed class Type {
|
||||||
|
@ -825,8 +816,8 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Mix-tape. These are usually [EP]-sized releases of music made to promote an [Artist]
|
* A Mix-tape. These are usually [EP]-sized releases of music made to promote an [Artist] or
|
||||||
* or a future release.
|
* a future release.
|
||||||
*/
|
*/
|
||||||
object Mixtape : Type() {
|
object Mixtape : Type() {
|
||||||
override val refinement: Refinement?
|
override val refinement: Refinement?
|
||||||
|
@ -836,18 +827,12 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
get() = R.string.lbl_mixtape
|
get() = R.string.lbl_mixtape
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A specification of what kind of performance a particular release is. */
|
||||||
* A specification of what kind of performance a particular release is.
|
|
||||||
*/
|
|
||||||
enum class Refinement {
|
enum class Refinement {
|
||||||
/**
|
/** A release consisting of a live performance */
|
||||||
* A release consisting of a live performance
|
|
||||||
*/
|
|
||||||
LIVE,
|
LIVE,
|
||||||
|
|
||||||
/**
|
/** A release consisting of another [Artist]s remix of a prior performance. */
|
||||||
* A release consisting of another [Artist]s remix of a prior performance.
|
|
||||||
*/
|
|
||||||
REMIX
|
REMIX
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -856,8 +841,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
* Parse a [Type] from a string formatted with the MusicBrainz Release Group Type
|
* Parse a [Type] from a string formatted with the MusicBrainz Release Group Type
|
||||||
* specification.
|
* specification.
|
||||||
* @param types A list of values consisting of valid release type values.
|
* @param types A list of values consisting of valid release type values.
|
||||||
* @return A [Type] consisting of the given types, or null if the types
|
* @return A [Type] consisting of the given types, or null if the types were not valid.
|
||||||
* were not valid.
|
|
||||||
*/
|
*/
|
||||||
fun parse(types: List<String>): Type? {
|
fun parse(types: List<String>): Type? {
|
||||||
val primary = types.getOrNull(0) ?: return null
|
val primary = types.getOrNull(0) ?: return null
|
||||||
|
@ -877,9 +861,9 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
* Parse "secondary" types (i.e not [Album], [EP], or [Single]) from a string formatted
|
* Parse "secondary" types (i.e not [Album], [EP], or [Single]) from a string formatted
|
||||||
* with the MusicBrainz Release Group Type specification.
|
* with the MusicBrainz Release Group Type specification.
|
||||||
* @param index The index of the release type to parse.
|
* @param index The index of the release type to parse.
|
||||||
* @param convertRefinement Code to convert a [Refinement] into a [Type]
|
* @param convertRefinement Code to convert a [Refinement] into a [Type] corresponding
|
||||||
* corresponding to the callee's context. This is used in order to handle secondary
|
* to the callee's context. This is used in order to handle secondary times that are
|
||||||
* times that are actually [Refinement]s.
|
* actually [Refinement]s.
|
||||||
* @return A [Type] corresponding to the secondary type found at that index.
|
* @return A [Type] corresponding to the secondary type found at that index.
|
||||||
*/
|
*/
|
||||||
private inline fun List<String>.parseSecondaryTypes(
|
private inline fun List<String>.parseSecondaryTypes(
|
||||||
|
@ -898,12 +882,12 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond
|
* Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond to
|
||||||
* to any child values.
|
* any child values.
|
||||||
* @param type The release type value to parse.
|
* @param type The release type value to parse.
|
||||||
* @param convertRefinement Code to convert a [Refinement] into a [Type]
|
* @param convertRefinement Code to convert a [Refinement] into a [Type] corresponding
|
||||||
* corresponding to the callee's context. This is used in order to handle secondary
|
* to the callee's context. This is used in order to handle secondary times that are
|
||||||
* times that are actually [Refinement]s.
|
* actually [Refinement]s.
|
||||||
*/
|
*/
|
||||||
private inline fun parseSecondaryTypeImpl(
|
private inline fun parseSecondaryTypeImpl(
|
||||||
type: String?,
|
type: String?,
|
||||||
|
@ -922,14 +906,13 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Raw information about an [Album] obtained from the component [Song] instances.
|
* Raw information about an [Album] obtained from the component [Song] instances. **This is only
|
||||||
* **This is only meant for use within the music package.**
|
* meant for use within the music package.**
|
||||||
*/
|
*/
|
||||||
class Raw(
|
class Raw(
|
||||||
/**
|
/**
|
||||||
* The ID of the [Album]'s grouping, obtained from MediaStore. Note that this
|
* The ID of the [Album]'s grouping, obtained from MediaStore. Note that this ID is highly
|
||||||
* ID is highly unstable and should only be used for accessing the system-provided
|
* unstable and should only be used for accessing the system-provided cover art.
|
||||||
* cover art.
|
|
||||||
*/
|
*/
|
||||||
val mediaStoreId: Long,
|
val mediaStoreId: Long,
|
||||||
/** @see Music.uid */
|
/** @see Music.uid */
|
||||||
|
@ -970,12 +953,12 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract artist. These are actually a combination of the artist and album artist tags
|
* An abstract artist. These are actually a combination of the artist and album artist tags from
|
||||||
* from within the library, derived from [Song]s and [Album]s respectively.
|
* within the library, derived from [Song]s and [Album]s respectively.
|
||||||
* @param raw The [Artist.Raw] to derive the member data from.
|
* @param raw The [Artist.Raw] to derive the member data from.
|
||||||
* @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist],
|
* @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist], either
|
||||||
* either through artist or album artist tags. Providing [Song]s to the artist is optional.
|
* through artist or album artist tags. Providing [Song]s to the artist is optional. These instances
|
||||||
* These instances will be linked to this [Artist].
|
* will be linked to this [Artist].
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicParent() {
|
||||||
|
@ -990,21 +973,21 @@ class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicP
|
||||||
override val songs: List<Song>
|
override val songs: List<Song>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All of the [Album]s this artist is credited to. Note that any [Song] credited to this
|
* All of the [Album]s this artist is credited to. Note that any [Song] credited to this artist
|
||||||
* artist will have it's [Album] considered to be "indirectly" linked to this [Artist], and
|
* will have it's [Album] considered to be "indirectly" linked to this [Artist], and thus
|
||||||
* thus included in this list.
|
* included in this list.
|
||||||
*/
|
*/
|
||||||
val albums: List<Album>
|
val albums: List<Album>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The duration of all [Song]s in the artist, in milliseconds.
|
* The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no
|
||||||
* Will be null if there are no songs.
|
* songs.
|
||||||
*/
|
*/
|
||||||
val durationMs: Long?
|
val durationMs: Long?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this artist is considered a "collaborator", i.e it is not directly credited on
|
* Whether this artist is considered a "collaborator", i.e it is not directly credited on any
|
||||||
* any [Album].
|
* [Album].
|
||||||
*/
|
*/
|
||||||
val isCollaborator: Boolean
|
val isCollaborator: Boolean
|
||||||
|
|
||||||
|
@ -1045,8 +1028,8 @@ class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicP
|
||||||
fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) }
|
fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the [Genre] *display* of this [Artist] and another [Artist] are equal. This
|
* Checks if the [Genre] *display* of this [Artist] and another [Artist] are equal. This will
|
||||||
* will only compare surface-level names, and not [Music.UID]s.
|
* only compare surface-level names, and not [Music.UID]s.
|
||||||
* @param other The [Artist] to compare to.
|
* @param other The [Artist] to compare to.
|
||||||
* @return True if the [Genre] displays are equal, false otherwise
|
* @return True if the [Genre] displays are equal, false otherwise
|
||||||
*/
|
*/
|
||||||
|
@ -1066,12 +1049,12 @@ class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicP
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the original position of this [Artist]'s [Artist.Raw] within the given [Artist.Raw]
|
* Returns the original position of this [Artist]'s [Artist.Raw] within the given [Artist.Raw]
|
||||||
* list. This can be used to create a consistent ordering within child [Artist] lists
|
* list. This can be used to create a consistent ordering within child [Artist] lists based on
|
||||||
* based on the original tag order.
|
* the original tag order.
|
||||||
* @param rawArtists The [Artist.Raw] instances to check. It is assumed that this [Artist]'s
|
* @param rawArtists The [Artist.Raw] instances to check. It is assumed that this [Artist]'s
|
||||||
* [Artist.Raw] will be within the list.
|
* [Artist.Raw] will be within the list.
|
||||||
* @return The index of the [Artist]'s [Artist.Raw] within the list.
|
* @return The index of the [Artist]'s [Artist.Raw] within the list. **This is only meant for
|
||||||
* **This is only meant for use within the music package.**
|
* use within the music package.**
|
||||||
*/
|
*/
|
||||||
fun _getOriginalPositionIn(rawArtists: List<Raw>) = rawArtists.indexOf(raw)
|
fun _getOriginalPositionIn(rawArtists: List<Raw>) = rawArtists.indexOf(raw)
|
||||||
|
|
||||||
|
@ -1138,19 +1121,13 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
|
||||||
override val collationKey = makeCollationKeyImpl()
|
override val collationKey = makeCollationKeyImpl()
|
||||||
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre)
|
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre)
|
||||||
|
|
||||||
/**
|
/** The albums indirectly linked to by the [Song]s of this [Genre]. */
|
||||||
* The albums indirectly linked to by the [Song]s of this [Genre].
|
|
||||||
*/
|
|
||||||
val albums: List<Album>
|
val albums: List<Album>
|
||||||
|
|
||||||
/**
|
/** The artists indirectly linked to by the [Artist]s of this [Genre]. */
|
||||||
* The artists indirectly linked to by the [Artist]s of this [Genre].
|
|
||||||
*/
|
|
||||||
val artists: List<Artist>
|
val artists: List<Artist>
|
||||||
|
|
||||||
/**
|
/** The total duration of the songs in this genre, in milliseconds. */
|
||||||
* The total duration of the songs in this genre, in milliseconds.
|
|
||||||
*/
|
|
||||||
val durationMs: Long
|
val durationMs: Long
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -1177,12 +1154,12 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the original position of this [Genre]'s [Genre.Raw] within the given [Genre.Raw]
|
* Returns the original position of this [Genre]'s [Genre.Raw] within the given [Genre.Raw]
|
||||||
* list. This can be used to create a consistent ordering within child [Genre] lists
|
* list. This can be used to create a consistent ordering within child [Genre] lists based on
|
||||||
* based on the original tag order.
|
* the original tag order.
|
||||||
* @param rawGenres The [Genre.Raw] instances to check. It is assumed that this [Genre]'s
|
* @param rawGenres The [Genre.Raw] instances to check. It is assumed that this [Genre]'s
|
||||||
* [Genre.Raw] will be within the list.
|
* [Genre.Raw] will be within the list.
|
||||||
* @return The index of the [Genre]'s [Genre.Raw] within the list.
|
* @return The index of the [Genre]'s [Genre.Raw] within the list. **This is only meant for use
|
||||||
* **This is only meant for use within the music package.**
|
* within the music package.**
|
||||||
*/
|
*/
|
||||||
fun _getOriginalPositionIn(rawGenres: List<Raw>) = rawGenres.indexOf(raw)
|
fun _getOriginalPositionIn(rawGenres: List<Raw>) = rawGenres.indexOf(raw)
|
||||||
|
|
||||||
|
@ -1191,13 +1168,11 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Raw information about a [Genre] obtained from the component [Song] instances.
|
* Raw information about a [Genre] obtained from the component [Song] instances. **This is only
|
||||||
* **This is only meant for use within the music package.**
|
* meant for use within the music package.**
|
||||||
*/
|
*/
|
||||||
class Raw(
|
class Raw(
|
||||||
/**
|
/** @see Music.rawName */
|
||||||
* @see Music.rawName
|
|
||||||
*/
|
|
||||||
val name: String? = null
|
val name: String? = null
|
||||||
) {
|
) {
|
||||||
// Only group by the lowercase genre name. This allows Genre grouping to be
|
// Only group by the lowercase genre name. This allows Genre grouping to be
|
||||||
|
@ -1219,13 +1194,11 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An ISO-8601/RFC 3339 Date.
|
* An ISO-8601/RFC 3339 Date.
|
||||||
*
|
*
|
||||||
* This class only encodes the timestamp spec and it's conversion to a human-readable date,
|
* This class only encodes the timestamp spec and it's conversion to a human-readable date, without
|
||||||
* without any other time management or validation. In general, this should only be used for
|
* any other time management or validation. In general, this should only be used for display.
|
||||||
* display.
|
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
@ -1240,16 +1213,17 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
/**
|
/**
|
||||||
* Resolve this instance into a human-readable date.
|
* Resolve this instance into a human-readable date.
|
||||||
* @param context [Context] required to get human-readable names.
|
* @param context [Context] required to get human-readable names.
|
||||||
* @return If the [Date] has a valid month and year value, a more fine-grained date
|
* @return If the [Date] has a valid month and year value, a more fine-grained date (ex. "Jan
|
||||||
* (ex. "Jan 2020") will be returned. Otherwise, a plain year value (ex. "2020") is
|
* 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will
|
||||||
* returned. Dates will be properly localized.
|
* be properly localized.
|
||||||
*/
|
*/
|
||||||
fun resolveDate(context: Context): String {
|
fun resolveDate(context: Context): String {
|
||||||
if (month != null) {
|
if (month != null) {
|
||||||
// Parse a date format from an ISO-ish format
|
// Parse a date format from an ISO-ish format
|
||||||
val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
|
val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat)
|
||||||
format.applyPattern("yyyy-MM")
|
format.applyPattern("yyyy-MM")
|
||||||
val date = try {
|
val date =
|
||||||
|
try {
|
||||||
format.parse("$year-$month")
|
format.parse("$year-$month")
|
||||||
} catch (e: ParseException) {
|
} catch (e: ParseException) {
|
||||||
null
|
null
|
||||||
|
@ -1307,8 +1281,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* A [Regex] that can parse a variable-precision ISO-8601 timestamp.
|
* A [Regex] that can parse a variable-precision ISO-8601 timestamp. Derived from
|
||||||
* Derived from https://github.com/quodlibet/mutagen
|
* https://github.com/quodlibet/mutagen
|
||||||
*/
|
*/
|
||||||
private val ISO8601_REGEX =
|
private val ISO8601_REGEX =
|
||||||
Regex(
|
Regex(
|
||||||
|
@ -1326,9 +1300,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
* @param year The year component.
|
* @param year The year component.
|
||||||
* @param month The month component.
|
* @param month The month component.
|
||||||
* @param day The day component.
|
* @param day The day component.
|
||||||
* @return A new [Date] consisting of the given components. May have reduced precision
|
* @return A new [Date] consisting of the given components. May have reduced precision if
|
||||||
* if the components were partially invalid, and will be null if all components are
|
* the components were partially invalid, and will be null if all components are invalid.
|
||||||
* invalid.
|
|
||||||
*/
|
*/
|
||||||
fun from(year: Int, month: Int, day: Int) = fromTokens(listOf(year, month, day))
|
fun from(year: Int, month: Int, day: Int) = fromTokens(listOf(year, month, day))
|
||||||
|
|
||||||
|
@ -1338,9 +1311,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
* @param month The month component.
|
* @param month The month component.
|
||||||
* @param day The day component.
|
* @param day The day component.
|
||||||
* @param hour The hour component
|
* @param hour The hour component
|
||||||
* @return A new [Date] consisting of the given components. May have reduced precision
|
* @return A new [Date] consisting of the given components. May have reduced precision if
|
||||||
* if the components were partially invalid, and will be null if all components are
|
* the components were partially invalid, and will be null if all components are invalid.
|
||||||
* invalid.
|
|
||||||
*/
|
*/
|
||||||
fun from(year: Int, month: Int, day: Int, hour: Int, minute: Int) =
|
fun from(year: Int, month: Int, day: Int, hour: Int, minute: Int) =
|
||||||
fromTokens(listOf(year, month, day, hour, minute))
|
fromTokens(listOf(year, month, day, hour, minute))
|
||||||
|
@ -1348,9 +1320,9 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
/**
|
/**
|
||||||
* Create a [Date] from a [String] timestamp.
|
* Create a [Date] from a [String] timestamp.
|
||||||
* @param timestamp The ISO-8601 timestamp to parse. Can have reduced precision.
|
* @param timestamp The ISO-8601 timestamp to parse. Can have reduced precision.
|
||||||
* @return A new [Date] consisting of the given components. May have reduced precision
|
* @return A new [Date] consisting of the given components. May have reduced precision if
|
||||||
* if the components were partially invalid, and will be null if all components are
|
* the components were partially invalid, and will be null if all components are invalid or
|
||||||
* invalid or if the timestamp is invalid.
|
* if the timestamp is invalid.
|
||||||
*/
|
*/
|
||||||
fun from(timestamp: String): Date? {
|
fun from(timestamp: String): Date? {
|
||||||
val tokens =
|
val tokens =
|
||||||
|
@ -1365,9 +1337,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
/**
|
/**
|
||||||
* Create a [Date] from the given non-validated tokens.
|
* Create a [Date] from the given non-validated tokens.
|
||||||
* @param tokens The tokens to use for each date component, in order of precision.
|
* @param tokens The tokens to use for each date component, in order of precision.
|
||||||
* @return A new [Date] consisting of the given components. May have reduced precision
|
* @return A new [Date] consisting of the given components. May have reduced precision if
|
||||||
* if the components were partially invalid, and will be null if all components are
|
* the components were partially invalid, and will be null if all components are invalid.
|
||||||
* invalid.
|
|
||||||
*/
|
*/
|
||||||
private fun fromTokens(tokens: List<Int>): Date? {
|
private fun fromTokens(tokens: List<Int>): Date? {
|
||||||
val validated = mutableListOf<Int>()
|
val validated = mutableListOf<Int>()
|
||||||
|
@ -1380,8 +1351,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a list of tokens provided by [src], and add the valid ones to [dst].
|
* Validate a list of tokens provided by [src], and add the valid ones to [dst]. Will stop
|
||||||
* Will stop as soon as an invalid token is found.
|
* as soon as an invalid token is found.
|
||||||
* @param src The input tokens to validate.
|
* @param src The input tokens to validate.
|
||||||
* @param dst The destination list to add valid tokens to.
|
* @param dst The destination list to add valid tokens to.
|
||||||
*/
|
*/
|
||||||
|
@ -1440,8 +1411,8 @@ private fun MessageDigest.update(n: Int?) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a [ByteArray] to a [UUID]. Assumes that the [ByteArray] has a length of 16.
|
* Convert a [ByteArray] to a [UUID]. Assumes that the [ByteArray] has a length of 16.
|
||||||
* @return A [UUID] derived from the [ByteArray]'s contents. Internally, the two [Long]s
|
* @return A [UUID] derived from the [ByteArray]'s contents. Internally, the two [Long]s in the
|
||||||
* in the [UUID] will be little-endian.
|
* [UUID] will be little-endian.
|
||||||
*/
|
*/
|
||||||
fun ByteArray.toUuid(): UUID {
|
fun ByteArray.toUuid(): UUID {
|
||||||
check(size == 16)
|
check(size == 16)
|
||||||
|
|
|
@ -20,15 +20,15 @@ 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.storage.useQuery
|
|
||||||
import org.oxycblt.auxio.music.storage.contentResolverSafe
|
import org.oxycblt.auxio.music.storage.contentResolverSafe
|
||||||
|
import org.oxycblt.auxio.music.storage.useQuery
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A repository granting access to the music library..
|
* A repository granting access to the music library..
|
||||||
*
|
*
|
||||||
* This can be used to obtain certain music items, or await changes to the music library.
|
* This can be used to obtain certain music items, or await changes to the music library. It is
|
||||||
* It is generally recommended to use this over Indexer to keep track of the library state,
|
* generally recommended to use this over Indexer to keep track of the library state, as the
|
||||||
* as the interface will be less volatile.
|
* interface will be less volatile.
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
@ -36,9 +36,9 @@ class MusicStore private constructor() {
|
||||||
private val callbacks = mutableListOf<Callback>()
|
private val callbacks = mutableListOf<Callback>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current [Library]. May be null if a [Library] has not been successfully loaded yet.
|
* The current [Library]. May be null if a [Library] has not been successfully loaded yet. This
|
||||||
* This can change, so it's highly recommended to not access this directly and instead
|
* can change, so it's highly recommended to not access this directly and instead rely on
|
||||||
* rely on [Callback].
|
* [Callback].
|
||||||
*/
|
*/
|
||||||
var library: Library? = null
|
var library: Library? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
|
@ -49,9 +49,8 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a [Callback] to this instance. This can be used to receive changes in the music
|
* Add a [Callback] to this instance. This can be used to receive changes in the music library.
|
||||||
* library. Will invoke all [Callback] methods to initialize the instance with the
|
* Will invoke all [Callback] methods to initialize the instance with the current state.
|
||||||
* current state.
|
|
||||||
* @param callback The [Callback] to add.
|
* @param callback The [Callback] to add.
|
||||||
* @see Callback
|
* @see Callback
|
||||||
*/
|
*/
|
||||||
|
@ -62,10 +61,9 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a [Callback] from this instance, preventing it from recieving any further
|
* Remove a [Callback] from this instance, preventing it from recieving any further updates.
|
||||||
* updates.
|
* @param callback The [Callback] to remove. Does nothing if the [Callback] was never added in
|
||||||
* @param callback The [Callback] to remove. Does nothing if the [Callback] was never
|
* the first place.
|
||||||
* added in the first place.
|
|
||||||
* @see Callback
|
* @see Callback
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
@ -116,8 +114,8 @@ class MusicStore private constructor() {
|
||||||
/**
|
/**
|
||||||
* Finds a [Music] item [T] in the library by it's [Music.UID].
|
* Finds a [Music] item [T] in the library by it's [Music.UID].
|
||||||
* @param uid The [Music.UID] to search for.
|
* @param uid The [Music.UID] to search for.
|
||||||
* @return The [T] corresponding to the given [Music.UID], or null if nothing could be
|
* @return The [T] corresponding to the given [Music.UID], or null if nothing could be found
|
||||||
* found or the [Music.UID] did not correspond to a [T].
|
* or the [Music.UID] did not correspond to a [T].
|
||||||
*/
|
*/
|
||||||
@Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID) = uidMap[uid] as? T
|
@Suppress("UNCHECKED_CAST") fun <T : Music> find(uid: Music.UID) = uidMap[uid] as? T
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ 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.system.Indexer
|
import org.oxycblt.auxio.music.system.Indexer
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [ViewModel] providing data specific to the music loading process.
|
* A [ViewModel] providing data specific to the music loading process.
|
||||||
|
|
|
@ -281,11 +281,13 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
|
|
||||||
override fun getSongComparator(isAscending: Boolean): Comparator<Song> =
|
override fun getSongComparator(isAscending: Boolean): Comparator<Song> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(isAscending) { it.durationMs }, compareBy(BasicComparator.SONG))
|
compareByDynamic(isAscending) { it.durationMs },
|
||||||
|
compareBy(BasicComparator.SONG))
|
||||||
|
|
||||||
override fun getAlbumComparator(isAscending: Boolean): Comparator<Album> =
|
override fun getAlbumComparator(isAscending: Boolean): Comparator<Album> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(isAscending) { it.durationMs }, compareBy(BasicComparator.ALBUM))
|
compareByDynamic(isAscending) { it.durationMs },
|
||||||
|
compareBy(BasicComparator.ALBUM))
|
||||||
|
|
||||||
override fun getArtistComparator(isAscending: Boolean): Comparator<Artist> =
|
override fun getArtistComparator(isAscending: Boolean): Comparator<Artist> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
|
@ -294,7 +296,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
|
|
||||||
override fun getGenreComparator(isAscending: Boolean): Comparator<Genre> =
|
override fun getGenreComparator(isAscending: Boolean): Comparator<Genre> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(isAscending) { it.durationMs }, compareBy(BasicComparator.GENRE))
|
compareByDynamic(isAscending) { it.durationMs },
|
||||||
|
compareBy(BasicComparator.GENRE))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -310,7 +313,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
|
|
||||||
override fun getAlbumComparator(isAscending: Boolean): Comparator<Album> =
|
override fun getAlbumComparator(isAscending: Boolean): Comparator<Album> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(isAscending) { it.songs.size }, compareBy(BasicComparator.ALBUM))
|
compareByDynamic(isAscending) { it.songs.size },
|
||||||
|
compareBy(BasicComparator.ALBUM))
|
||||||
|
|
||||||
override fun getArtistComparator(isAscending: Boolean): Comparator<Artist> =
|
override fun getArtistComparator(isAscending: Boolean): Comparator<Artist> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
|
@ -319,7 +323,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
|
|
||||||
override fun getGenreComparator(isAscending: Boolean): Comparator<Genre> =
|
override fun getGenreComparator(isAscending: Boolean): Comparator<Genre> =
|
||||||
MultiComparator(
|
MultiComparator(
|
||||||
compareByDynamic(isAscending) { it.songs.size }, compareBy(BasicComparator.GENRE))
|
compareByDynamic(isAscending) { it.songs.size },
|
||||||
|
compareBy(BasicComparator.GENRE))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -430,8 +435,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility function to create a [Comparator] that sorts in ascending order based on
|
* Utility function to create a [Comparator] that sorts in ascending order based on the
|
||||||
* the given [Comparator], with a selector based on the item itself.
|
* given [Comparator], with a selector based on the item itself.
|
||||||
* @param comparator The [Comparator] to wrap.
|
* @param comparator The [Comparator] to wrap.
|
||||||
* @return A new [Comparator] with the specified configuration.
|
* @return A new [Comparator] with the specified configuration.
|
||||||
* @see compareBy
|
* @see compareBy
|
||||||
|
@ -440,11 +445,9 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
compareBy(comparator) { it }
|
compareBy(comparator) { it }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Comparator] that chains several other [Comparator]s together to form one
|
* A [Comparator] that chains several other [Comparator]s together to form one comparison.
|
||||||
* comparison.
|
* @param comparators The [Comparator]s to chain. These will be iterated through in order
|
||||||
* @param comparators The [Comparator]s to chain. These will be iterated through
|
* during a comparison, with the first non-equal result becoming the result.
|
||||||
* in order during a comparison, with the first non-equal result becoming the
|
|
||||||
* result.
|
|
||||||
*/
|
*/
|
||||||
private class MultiComparator<T>(vararg comparators: Comparator<T>) : Comparator<T> {
|
private class MultiComparator<T>(vararg comparators: Comparator<T>) : Comparator<T> {
|
||||||
private val _comparators = comparators
|
private val _comparators = comparators
|
||||||
|
@ -493,8 +496,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Comparator] that compares abstract [Music] values. Internally, this is similar
|
* A [Comparator] that compares abstract [Music] values. Internally, this is similar to
|
||||||
* to [NullableComparator], however comparing [Music.collationKey] instead of [Comparable].
|
* [NullableComparator], however comparing [Music.collationKey] instead of [Comparable].
|
||||||
* @see NullableComparator
|
* @see NullableComparator
|
||||||
* @see Music.collationKey
|
* @see Music.collationKey
|
||||||
*/
|
*/
|
||||||
|
@ -523,8 +526,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Comparator] that compares two possibly null values. Values will be considered
|
* A [Comparator] that compares two possibly null values. Values will be considered lesser
|
||||||
* lesser if they are null, and greater if they are non-null.
|
* if they are null, and greater if they are non-null.
|
||||||
*/
|
*/
|
||||||
private class NullableComparator<T : Comparable<T>> private constructor() : Comparator<T?> {
|
private class NullableComparator<T : Comparable<T>> private constructor() : Comparator<T?> {
|
||||||
override fun compare(a: T?, b: T?) =
|
override fun compare(a: T?, b: T?) =
|
||||||
|
|
|
@ -38,22 +38,20 @@ import org.oxycblt.auxio.util.requireBackgroundThread
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
interface CacheExtractor {
|
interface CacheExtractor {
|
||||||
/**
|
/** Initialize the Extractor by reading the cache data into memory. */
|
||||||
* Initialize the Extractor by reading the cache data into memory.
|
|
||||||
*/
|
|
||||||
fun init()
|
fun init()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache,
|
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside
|
||||||
* alongside freeing up memory.
|
* freeing up memory.
|
||||||
* @param rawSongs The songs to write into the cache.
|
* @param rawSongs The songs to write into the cache.
|
||||||
*/
|
*/
|
||||||
fun finalize(rawSongs: List<Song.Raw>)
|
fun finalize(rawSongs: List<Song.Raw>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use the cache to populate the given [Song.Raw].
|
* Use the cache to populate the given [Song.Raw].
|
||||||
* @param rawSong The [Song.Raw] to attempt to populate. Note that this [Song.Raw] will
|
* @param rawSong The [Song.Raw] to attempt to populate. Note that this [Song.Raw] will only
|
||||||
* only contain the bare minimum information required to load a cache entry.
|
* contain the bare minimum information required to load a cache entry.
|
||||||
* @return An [ExtractionResult] representing the result of the operation.
|
* @return An [ExtractionResult] representing the result of the operation.
|
||||||
* [ExtractionResult.PARSED] is not returned.
|
* [ExtractionResult.PARSED] is not returned.
|
||||||
*/
|
*/
|
||||||
|
@ -61,8 +59,8 @@ interface CacheExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [CacheExtractor] only capable of writing to the cache. This can be used to load music
|
* A [CacheExtractor] only capable of writing to the cache. This can be used to load music with
|
||||||
* with without the cache if the user desires.
|
* without the cache if the user desires.
|
||||||
* @param context [Context] required to read the cache database.
|
* @param context [Context] required to read the cache database.
|
||||||
* @see CacheExtractor
|
* @see CacheExtractor
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
@ -120,7 +118,8 @@ class ReadWriteCacheExtractor(private val context: Context) : WriteOnlyCacheExtr
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun populate(rawSong: Song.Raw): ExtractionResult {
|
override fun populate(rawSong: Song.Raw): ExtractionResult {
|
||||||
val map = requireNotNull(cacheMap) {
|
val map =
|
||||||
|
requireNotNull(cacheMap) {
|
||||||
"Must initialize this extractor before populating a raw song."
|
"Must initialize this extractor before populating a raw song."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,8 +227,8 @@ private class CacheDatabase(context: Context) :
|
||||||
/**
|
/**
|
||||||
* Read out this database into memory.
|
* Read out this database into memory.
|
||||||
* @return A mapping between the MediaStore IDs of the cache entries and a [Song.Raw] containing
|
* @return A mapping between the MediaStore IDs of the cache entries and a [Song.Raw] containing
|
||||||
* the cacheable data for the entry. Note that any filesystem-dependent information
|
* the cacheable data for the entry. Note that any filesystem-dependent information (excluding
|
||||||
* (excluding IDs and timestamps) is not cached.
|
* IDs and timestamps) is not cached.
|
||||||
*/
|
*/
|
||||||
fun read(): Map<Long, Song.Raw> {
|
fun read(): Map<Long, Song.Raw> {
|
||||||
requireBackgroundThread()
|
requireBackgroundThread()
|
||||||
|
@ -323,7 +322,9 @@ private class CacheDatabase(context: Context) :
|
||||||
raw.albumArtistSortNames = it.parseSQLMultiValue()
|
raw.albumArtistSortNames = it.parseSQLMultiValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.getStringOrNull(genresIndex)?.let { raw.genreNames = it.parseSQLMultiValue() }
|
cursor.getStringOrNull(genresIndex)?.let {
|
||||||
|
raw.genreNames = it.parseSQLMultiValue()
|
||||||
|
}
|
||||||
|
|
||||||
map[id] = raw
|
map[id] = raw
|
||||||
}
|
}
|
||||||
|
@ -376,20 +377,22 @@ private class CacheDatabase(context: Context) :
|
||||||
put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId)
|
put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId)
|
||||||
put(Columns.ALBUM_NAME, rawSong.albumName)
|
put(Columns.ALBUM_NAME, rawSong.albumName)
|
||||||
put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName)
|
put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName)
|
||||||
put(
|
put(Columns.ALBUM_TYPES, rawSong.albumTypes.toSQLMultiValue())
|
||||||
Columns.ALBUM_TYPES,
|
|
||||||
rawSong.albumTypes.toSQLMultiValue())
|
|
||||||
|
|
||||||
put(
|
put(
|
||||||
Columns.ARTIST_MUSIC_BRAINZ_IDS,
|
Columns.ARTIST_MUSIC_BRAINZ_IDS,
|
||||||
rawSong.artistMusicBrainzIds.toSQLMultiValue())
|
rawSong.artistMusicBrainzIds.toSQLMultiValue())
|
||||||
put(Columns.ARTIST_NAMES, rawSong.artistNames.toSQLMultiValue())
|
put(Columns.ARTIST_NAMES, rawSong.artistNames.toSQLMultiValue())
|
||||||
put(Columns.ARTIST_SORT_NAMES, rawSong.artistSortNames.toSQLMultiValue())
|
put(
|
||||||
|
Columns.ARTIST_SORT_NAMES,
|
||||||
|
rawSong.artistSortNames.toSQLMultiValue())
|
||||||
|
|
||||||
put(
|
put(
|
||||||
Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS,
|
Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS,
|
||||||
rawSong.albumArtistMusicBrainzIds.toSQLMultiValue())
|
rawSong.albumArtistMusicBrainzIds.toSQLMultiValue())
|
||||||
put(Columns.ALBUM_ARTIST_NAMES, rawSong.albumArtistNames.toSQLMultiValue())
|
put(
|
||||||
|
Columns.ALBUM_ARTIST_NAMES,
|
||||||
|
rawSong.albumArtistNames.toSQLMultiValue())
|
||||||
put(
|
put(
|
||||||
Columns.ALBUM_ARTIST_SORT_NAMES,
|
Columns.ALBUM_ARTIST_SORT_NAMES,
|
||||||
rawSong.albumArtistSortNames.toSQLMultiValue())
|
rawSong.albumArtistSortNames.toSQLMultiValue())
|
||||||
|
@ -416,8 +419,8 @@ private class CacheDatabase(context: Context) :
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms the multi-string list into a SQL-safe multi-string value.
|
* Transforms the multi-string list into a SQL-safe multi-string value.
|
||||||
* @return A single string containing all values within the multi-string list, delimited
|
* @return A single string containing all values within the multi-string list, delimited by a
|
||||||
* by a ";". Pre-existing ";" characters will be escaped.
|
* ";". Pre-existing ";" characters will be escaped.
|
||||||
*/
|
*/
|
||||||
private fun List<String>.toSQLMultiValue() =
|
private fun List<String>.toSQLMultiValue() =
|
||||||
if (isNotEmpty()) {
|
if (isNotEmpty()) {
|
||||||
|
@ -428,14 +431,12 @@ private class CacheDatabase(context: Context) :
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms the SQL-safe multi-string value into a multi-string list.
|
* Transforms the SQL-safe multi-string value into a multi-string list.
|
||||||
* @return A list of strings corresponding to the delimited values present within the
|
* @return A list of strings corresponding to the delimited values present within the original
|
||||||
* original string. Escaped delimiters are converted back into their normal forms.
|
* string. Escaped delimiters are converted back into their normal forms.
|
||||||
*/
|
*/
|
||||||
private fun String.parseSQLMultiValue() = splitEscaped { it == ';' }
|
private fun String.parseSQLMultiValue() = splitEscaped { it == ';' }
|
||||||
|
|
||||||
/**
|
/** Defines the columns used in this database. */
|
||||||
* Defines the columns used in this database.
|
|
||||||
*/
|
|
||||||
private object Columns {
|
private object Columns {
|
||||||
/** @see Song.Raw.mediaStoreId */
|
/** @see Song.Raw.mediaStoreId */
|
||||||
const val MEDIA_STORE_ID = "msid"
|
const val MEDIA_STORE_ID = "msid"
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.extractor
|
package org.oxycblt.auxio.music.extractor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,18 +22,12 @@ package org.oxycblt.auxio.music.extractor
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
enum class ExtractionResult {
|
enum class ExtractionResult {
|
||||||
/**
|
/** A raw song was successfully extracted from the cache. */
|
||||||
* A raw song was successfully extracted from the cache.
|
|
||||||
*/
|
|
||||||
CACHED,
|
CACHED,
|
||||||
|
|
||||||
/**
|
/** A raw song was successfully extracted from parsing it's file. */
|
||||||
* A raw song was successfully extracted from parsing it's file.
|
|
||||||
*/
|
|
||||||
PARSED,
|
PARSED,
|
||||||
|
|
||||||
/**
|
/** A raw song could not be parsed. */
|
||||||
* A raw song could not be parsed.
|
|
||||||
*/
|
|
||||||
NONE
|
NONE
|
||||||
}
|
}
|
|
@ -29,21 +29,21 @@ import androidx.core.database.getStringOrNull
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.storage.Directory
|
import org.oxycblt.auxio.music.storage.Directory
|
||||||
|
import org.oxycblt.auxio.music.storage.contentResolverSafe
|
||||||
import org.oxycblt.auxio.music.storage.directoryCompat
|
import org.oxycblt.auxio.music.storage.directoryCompat
|
||||||
import org.oxycblt.auxio.music.storage.mediaStoreVolumeNameCompat
|
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.settings.Settings
|
||||||
import org.oxycblt.auxio.music.storage.contentResolverSafe
|
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The layer that loads music from the [MediaStore] database. This is an intermediate step in the
|
* The layer that loads music from the [MediaStore] database. This is an intermediate step in the
|
||||||
* music extraction process and primarily intended for redundancy for files not natively
|
* music extraction process and primarily intended for redundancy for files not natively supported
|
||||||
* supported by [MetadataExtractor]. Solely relying on this is not recommended, as it often
|
* by [MetadataExtractor]. Solely relying on this is not recommended, as it often produces bad
|
||||||
* produces bad metadata.
|
* metadata.
|
||||||
* @param context [Context] required to query the media database.
|
* @param context [Context] required to query the media database.
|
||||||
* @param cacheExtractor [CacheExtractor] implementation for cache optimizations.
|
* @param cacheExtractor [CacheExtractor] implementation for cache optimizations.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
@ -69,15 +69,15 @@ abstract class MediaStoreExtractor(
|
||||||
private val genreNamesMap = mutableMapOf<Long, String>()
|
private val genreNamesMap = mutableMapOf<Long, String>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [StorageVolume]s currently scanned by [MediaStore]. This should be used to transform
|
* The [StorageVolume]s currently scanned by [MediaStore]. This should be used to transform path
|
||||||
* path information from the database into volume-aware paths.
|
* information from the database into volume-aware paths.
|
||||||
*/
|
*/
|
||||||
protected var volumes = listOf<StorageVolume>()
|
protected var volumes = listOf<StorageVolume>()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize this instance. This involves setting up the required sub-extractors and
|
* Initialize this instance. This involves setting up the required sub-extractors and querying
|
||||||
* querying the media database for music files.
|
* the media database for music files.
|
||||||
* @return A [Cursor] of the music data returned from the database.
|
* @return A [Cursor] of the music data returned from the database.
|
||||||
*/
|
*/
|
||||||
open fun init(): Cursor {
|
open fun init(): Cursor {
|
||||||
|
@ -124,11 +124,14 @@ abstract class MediaStoreExtractor(
|
||||||
|
|
||||||
// Now we can actually query MediaStore.
|
// Now we can actually query MediaStore.
|
||||||
logD("Starting song query [proj: ${projection.toList()}, selector: $selector, args: $args]")
|
logD("Starting song query [proj: ${projection.toList()}, selector: $selector, args: $args]")
|
||||||
val cursor = context.contentResolverSafe.safeQuery(
|
val cursor =
|
||||||
|
context.contentResolverSafe
|
||||||
|
.safeQuery(
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
projection,
|
projection,
|
||||||
selector,
|
selector,
|
||||||
args.toTypedArray()).also { cursor = it }
|
args.toTypedArray())
|
||||||
|
.also { cursor = it }
|
||||||
logD("Song query succeeded [Projected total: ${cursor.count}]")
|
logD("Song query succeeded [Projected total: ${cursor.count}]")
|
||||||
|
|
||||||
// Set up cursor indices for later use.
|
// Set up cursor indices for later use.
|
||||||
|
@ -184,8 +187,8 @@ abstract class MediaStoreExtractor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache,
|
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside
|
||||||
* alongside freeing up memory.
|
* freeing up memory.
|
||||||
* @param rawSongs The songs to write into the cache.
|
* @param rawSongs The songs to write into the cache.
|
||||||
*/
|
*/
|
||||||
fun finalize(rawSongs: List<Song.Raw>) {
|
fun finalize(rawSongs: List<Song.Raw>) {
|
||||||
|
@ -222,8 +225,8 @@ abstract class MediaStoreExtractor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The database columns available to all android versions supported by Auxio.
|
* The database columns available to all android versions supported by Auxio. Concrete
|
||||||
* Concrete implementations can extend this projection to add version-specific columns.
|
* implementations can extend this projection to add version-specific columns.
|
||||||
*/
|
*/
|
||||||
protected open val projection: Array<String>
|
protected open val projection: Array<String>
|
||||||
get() =
|
get() =
|
||||||
|
@ -244,8 +247,8 @@ abstract class MediaStoreExtractor(
|
||||||
AUDIO_COLUMN_ALBUM_ARTIST)
|
AUDIO_COLUMN_ALBUM_ARTIST)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The companion template to add to the projection's selector whenever arguments are added
|
* The companion template to add to the projection's selector whenever arguments are added by
|
||||||
* by [addDirToSelector].
|
* [addDirToSelector].
|
||||||
* @see addDirToSelector
|
* @see addDirToSelector
|
||||||
*/
|
*/
|
||||||
protected abstract val dirSelectorTemplate: String
|
protected abstract val dirSelectorTemplate: String
|
||||||
|
@ -253,8 +256,8 @@ abstract class MediaStoreExtractor(
|
||||||
/**
|
/**
|
||||||
* Add a [Directory] to the given list of projection selector arguments.
|
* Add a [Directory] to the given list of projection selector arguments.
|
||||||
* @param dir The [Directory] to add.
|
* @param dir The [Directory] to add.
|
||||||
* @param args The destination list to append selector arguments to that are analogous
|
* @param args The destination list to append selector arguments to that are analogous to the
|
||||||
* to the given [Directory].
|
* given [Directory].
|
||||||
* @return true if the [Directory] was added, false otherwise.
|
* @return true if the [Directory] was added, false otherwise.
|
||||||
* @see dirSelectorTemplate
|
* @see dirSelectorTemplate
|
||||||
*/
|
*/
|
||||||
|
@ -263,8 +266,8 @@ abstract class MediaStoreExtractor(
|
||||||
/**
|
/**
|
||||||
* Populate a [Song.Raw] with the "File Data" of the given [MediaStore] [Cursor], which is the
|
* Populate a [Song.Raw] with the "File Data" of the given [MediaStore] [Cursor], which is the
|
||||||
* data that cannot be cached. This includes any information not intrinsic to the file and
|
* data that cannot be cached. This includes any information not intrinsic to the file and
|
||||||
* instead dependent on the file-system, which could change without invalidating the cache
|
* instead dependent on the file-system, which could change without invalidating the cache due
|
||||||
* due to volume additions or removals.
|
* to volume additions or removals.
|
||||||
* @param cursor The [Cursor] to read from.
|
* @param cursor The [Cursor] to read from.
|
||||||
* @param raw The [Song.Raw] to populate.
|
* @param raw The [Song.Raw] to populate.
|
||||||
* @see populateMetadata
|
* @see populateMetadata
|
||||||
|
@ -281,9 +284,9 @@ abstract class MediaStoreExtractor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populate a [Song.Raw] with the Metadata of the given [MediaStore] [Cursor], which is the
|
* Populate a [Song.Raw] with the Metadata of the given [MediaStore] [Cursor], which is the data
|
||||||
* data about a [Song.Raw] that can be cached. This includes any information intrinsic to
|
* about a [Song.Raw] that can be cached. This includes any information intrinsic to the file or
|
||||||
* the file or it's file format, such as music tags.
|
* it's file format, such as music tags.
|
||||||
* @param cursor The [Cursor] to read from.
|
* @param cursor The [Cursor] to read from.
|
||||||
* @param raw The [Song.Raw] to populate.
|
* @param raw The [Song.Raw] to populate.
|
||||||
* @see populateFileData
|
* @see populateFileData
|
||||||
|
@ -334,8 +337,8 @@ abstract class MediaStoreExtractor(
|
||||||
private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST
|
private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The external volume. This naming has existed since API 21, but no constant existed
|
* The external volume. This naming has existed since API 21, but no constant existed for it
|
||||||
* for it until API 29. This will work on all versions that Auxio supports.
|
* until API 29. This will work on all versions that Auxio supports.
|
||||||
*/
|
*/
|
||||||
@Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
|
@Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
|
||||||
}
|
}
|
||||||
|
@ -367,7 +370,8 @@ class Api21MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor)
|
||||||
override val projection: Array<String>
|
override val projection: Array<String>
|
||||||
get() =
|
get() =
|
||||||
super.projection +
|
super.projection +
|
||||||
arrayOf(MediaStore.Audio.AudioColumns.TRACK,
|
arrayOf(
|
||||||
|
MediaStore.Audio.AudioColumns.TRACK,
|
||||||
// Below API 29, we are restricted to the absolute path (Called DATA by
|
// Below API 29, we are restricted to the absolute path (Called DATA by
|
||||||
// MedaStore) when working with audio files.
|
// MedaStore) when working with audio files.
|
||||||
MediaStore.Audio.AudioColumns.DATA)
|
MediaStore.Audio.AudioColumns.DATA)
|
||||||
|
@ -486,8 +490,8 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheExtractor: CacheE
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [MediaStoreExtractor] that completes the music loading process in a way compatible with at
|
* A [MediaStoreExtractor] that completes the music loading process in a way compatible with at API
|
||||||
* API 29.
|
* 29.
|
||||||
* @param context [Context] required to query the media database.
|
* @param context [Context] required to query the media database.
|
||||||
* @param cacheExtractor [CacheExtractor] implementation for cache functionality.
|
* @param cacheExtractor [CacheExtractor] implementation for cache functionality.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
@ -521,8 +525,8 @@ open class Api29MediaStoreExtractor(context: Context, cacheExtractor: CacheExtra
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [MediaStoreExtractor] that completes the music loading process in a way compatible from
|
* A [MediaStoreExtractor] that completes the music loading process in a way compatible from API 30
|
||||||
* API 30 onwards.
|
* onwards.
|
||||||
* @param context [Context] required to query the media database.
|
* @param context [Context] required to query the media database.
|
||||||
* @param cacheExtractor [CacheExtractor] implementation for cache optimizations.
|
* @param cacheExtractor [CacheExtractor] implementation for cache optimizations.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
|
|
@ -32,8 +32,8 @@ import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the
|
* The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the
|
||||||
* last step in the music extraction process and is mostly responsible for papering over the
|
* last step in the music extraction process and is mostly responsible for papering over the bad
|
||||||
* bad metadata that [MediaStoreExtractor] produces.
|
* metadata that [MediaStoreExtractor] produces.
|
||||||
*
|
*
|
||||||
* @param context [Context] required for reading audio files.
|
* @param context [Context] required for reading audio files.
|
||||||
* @param mediaStoreExtractor [MediaStoreExtractor] implementation for cache optimizations and
|
* @param mediaStoreExtractor [MediaStoreExtractor] implementation for cache optimizations and
|
||||||
|
@ -56,17 +56,17 @@ class MetadataExtractor(
|
||||||
fun init() = mediaStoreExtractor.init().count
|
fun init() = mediaStoreExtractor.init().count
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache,
|
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside
|
||||||
* alongside freeing up memory.
|
* freeing up memory.
|
||||||
* @param rawSongs The songs to write into the cache.
|
* @param rawSongs The songs to write into the cache.
|
||||||
*/
|
*/
|
||||||
fun finalize(rawSongs: List<Song.Raw>) = mediaStoreExtractor.finalize(rawSongs)
|
fun finalize(rawSongs: List<Song.Raw>) = mediaStoreExtractor.finalize(rawSongs)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse all [Song.Raw] instances queued by the sub-extractors. This will first delegate
|
* Parse all [Song.Raw] instances queued by the sub-extractors. This will first delegate to the
|
||||||
* to the sub-extractors before parsing the metadata itself.
|
* sub-extractors before parsing the metadata itself.
|
||||||
* @param emit A callback that will be invoked with every new [Song.Raw] instance when
|
* @param emit A callback that will be invoked with every new [Song.Raw] instance when they are
|
||||||
* they are successfully loaded.
|
* successfully loaded.
|
||||||
*/
|
*/
|
||||||
suspend fun parse(emit: suspend (Song.Raw) -> Unit) {
|
suspend fun parse(emit: suspend (Song.Raw) -> Unit) {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -122,8 +122,8 @@ class MetadataExtractor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a [MetadataExtractor] future and processes it into a [Song.Raw] when completed.
|
* Wraps a [MetadataExtractor] future and processes it into a [Song.Raw] when completed. TODO:
|
||||||
* TODO: Re-unify with MetadataExtractor.
|
* Re-unify with MetadataExtractor.
|
||||||
* @param context [Context] required to open the audio file.
|
* @param context [Context] required to open the audio file.
|
||||||
* @param raw [Song.Raw] to process.
|
* @param raw [Song.Raw] to process.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
@ -135,7 +135,8 @@ class Task(context: Context, private val raw: Song.Raw) {
|
||||||
private val future =
|
private val future =
|
||||||
MetadataRetriever.retrieveMetadata(
|
MetadataRetriever.retrieveMetadata(
|
||||||
context,
|
context,
|
||||||
MediaItem.fromUri(requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri()))
|
MediaItem.fromUri(
|
||||||
|
requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri()))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to get a completed song from this [Task], if it has finished processing.
|
* Try to get a completed song from this [Task], if it has finished processing.
|
||||||
|
@ -246,14 +247,17 @@ class Task(context: Context, private val raw: Song.Raw) {
|
||||||
// 5. ID3v2.3 Release Year, as it is the most common date type
|
// 5. ID3v2.3 Release Year, as it is the most common date type
|
||||||
(textFrames["TDOR"]?.run { get(0).parseTimestamp() }
|
(textFrames["TDOR"]?.run { get(0).parseTimestamp() }
|
||||||
?: textFrames["TDRC"]?.run { get(0).parseTimestamp() }
|
?: textFrames["TDRC"]?.run { get(0).parseTimestamp() }
|
||||||
?: textFrames["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(textFrames))
|
?: textFrames["TDRL"]?.run { get(0).parseTimestamp() }
|
||||||
|
?: parseId3v23Date(textFrames))
|
||||||
?.let { raw.date = it }
|
?.let { raw.date = it }
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
textFrames["TXXX:MusicBrainz Album Id"]?.let { raw.albumMusicBrainzId = it[0] }
|
textFrames["TXXX:MusicBrainz Album Id"]?.let { raw.albumMusicBrainzId = it[0] }
|
||||||
textFrames["TALB"]?.let { raw.albumName = it[0] }
|
textFrames["TALB"]?.let { raw.albumName = it[0] }
|
||||||
textFrames["TSOA"]?.let { raw.albumSortName = it[0] }
|
textFrames["TSOA"]?.let { raw.albumSortName = it[0] }
|
||||||
(textFrames["TXXX:MusicBrainz Album Type"] ?: textFrames["GRP1"])?.let { raw.albumTypes = it }
|
(textFrames["TXXX:MusicBrainz Album Type"] ?: textFrames["GRP1"])?.let {
|
||||||
|
raw.albumTypes = it
|
||||||
|
}
|
||||||
|
|
||||||
// Artist
|
// Artist
|
||||||
textFrames["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it }
|
textFrames["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it }
|
||||||
|
@ -274,9 +278,9 @@ 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,
|
* @retrn A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a
|
||||||
* and a hour/minute value from TIME. No second value is included. The latter two fields may
|
* hour/minute value from TIME. No second value is included. The latter two fields may not be
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
private fun parseId3v23Date(textFrames: Map<String, List<String>>): Date? {
|
private fun parseId3v23Date(textFrames: Map<String, List<String>>): Date? {
|
||||||
// Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY
|
// Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY
|
||||||
|
@ -313,8 +317,7 @@ class Task(context: Context, private val raw: Song.Raw) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete this instance's [Song.Raw] with Vorbis comments.
|
* Complete this instance's [Song.Raw] with Vorbis comments.
|
||||||
* @param comments A mapping between vorbis comment names and one or more vorbis comment
|
* @param comments A mapping between vorbis comment names and one or more vorbis comment values.
|
||||||
* values.
|
|
||||||
*/
|
*/
|
||||||
private fun populateWithVorbis(comments: Map<String, List<String>>) {
|
private fun populateWithVorbis(comments: Map<String, List<String>>) {
|
||||||
// Song
|
// Song
|
||||||
|
@ -363,8 +366,8 @@ class Task(context: Context, private val raw: Song.Raw) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies and sanitizes a possibly native/non-UTF-8 string.
|
* Copies and sanitizes a possibly native/non-UTF-8 string.
|
||||||
* @return A new string allocated in a memory-safe manner with any UTF-8 errors
|
* @return A new string allocated in a memory-safe manner with any UTF-8 errors replaced with
|
||||||
* replaced with the Unicode replacement byte sequence.
|
* the Unicode replacement byte sequence.
|
||||||
*/
|
*/
|
||||||
private fun String.sanitize() = String(encodeToByteArray())
|
private fun String.sanitize() = String(encodeToByteArray())
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,27 +24,25 @@ import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unpack the track number from a combined track + disc [Int] field.
|
* Unpack the track number from a combined track + disc [Int] field. These fields appear within
|
||||||
* These fields appear within MediaStore's TRACK column, and combine the track and disc value
|
* MediaStore's TRACK column, and combine the track and disc value into a single field where the
|
||||||
* into a single field where the disc number is the 4th+ digit.
|
* disc number is the 4th+ digit.
|
||||||
* @return The track number extracted from the combined integer value, or null if the value
|
* @return The track number extracted from the combined integer value, or null if the value was
|
||||||
* was zero.
|
* zero.
|
||||||
*/
|
*/
|
||||||
fun Int.unpackTrackNo() = mod(1000).nonZeroOrNull()
|
fun Int.unpackTrackNo() = mod(1000).nonZeroOrNull()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unpack the disc number from a combined track + disc [Int] field.
|
* Unpack the disc number from a combined track + disc [Int] field. These fields appear within
|
||||||
* These fields appear within MediaStore's TRACK column, and combine the track and disc value
|
* MediaStore's TRACK column, and combine the track and disc value into a single field where the
|
||||||
* into a single field where the disc number is the 4th+ digit.
|
* disc number is the 4th+ digit.
|
||||||
* @return The disc number extracted from the combined integer field, or null if the value
|
* @return The disc number extracted from the combined integer field, or null if the value was zero.
|
||||||
* was zero.
|
|
||||||
*/
|
*/
|
||||||
fun Int.unpackDiscNo() = div(1000).nonZeroOrNull()
|
fun Int.unpackDiscNo() = div(1000).nonZeroOrNull()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the number out of a combined number + total position [String] field.
|
* Parse the number out of a combined number + total position [String] field. These fields often
|
||||||
* These fields often appear in ID3v2 files, and consist of a number and an (optional) total
|
* appear in ID3v2 files, and consist of a number and an (optional) total value delimited by a /.
|
||||||
* value delimited by a /.
|
|
||||||
* @return The number value extracted from the string field, or null if the value could not be
|
* @return The number value extracted from the string field, or null if the value could not be
|
||||||
* parsed or if the value was zero.
|
* parsed or if the value was zero.
|
||||||
*/
|
*/
|
||||||
|
@ -59,24 +57,23 @@ fun Int.toDate() = Date.from(this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse an integer year field from a [String] and transform it into a [Date].
|
* Parse an integer year field from a [String] and transform it into a [Date].
|
||||||
* @return A [Date] consisting of the year value, or null if the value could not
|
* @return A [Date] consisting of the year value, or null if the value could not be parsed or if the
|
||||||
* be parsed or if the value was zero.
|
* value was zero.
|
||||||
* @see Date.from
|
* @see Date.from
|
||||||
*/
|
*/
|
||||||
fun String.parseYear() = toIntOrNull()?.toDate()
|
fun String.parseYear() = toIntOrNull()?.toDate()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse an ISO-8601 timestamp [String] into a [Date].
|
* Parse an ISO-8601 timestamp [String] into a [Date].
|
||||||
* @return A [Date] consisting of the year value plus one or more refinement values
|
* @return A [Date] consisting of the year value plus one or more refinement values (ex. month,
|
||||||
* (ex. month, day), or null if the timestamp was not valid.
|
* day), or null if the timestamp was not valid.
|
||||||
*/
|
*/
|
||||||
fun String.parseTimestamp() = Date.from(this)
|
fun String.parseTimestamp() = Date.from(this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split a [String] by the given selector, automatically handling escaped characters
|
* Split a [String] by the given selector, automatically handling escaped characters that satisfy
|
||||||
* that satisfy the selector.
|
* the selector.
|
||||||
* @param selector A block that determines if the string should be split at a given
|
* @param selector A block that determines if the string should be split at a given character.
|
||||||
* character.
|
|
||||||
* @return One or more [String]s split by the selector.
|
* @return One or more [String]s split by the selector.
|
||||||
*/
|
*/
|
||||||
inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String> {
|
inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String> {
|
||||||
|
@ -118,9 +115,9 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a multi-value tag based on the user configuration. If the value is already composed of
|
* Parse a multi-value tag based on the user configuration. If the value is already composed of more
|
||||||
* more than one value, nothing is done. Otherwise, this function will attempt to split it based
|
* than one value, nothing is done. Otherwise, this function will attempt to split it based on the
|
||||||
* on the user's separator preferences.
|
* user's separator preferences.
|
||||||
* @param settings [Settings] required to obtain user separator configuration.
|
* @param settings [Settings] 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.
|
||||||
*/
|
*/
|
||||||
|
@ -157,8 +154,8 @@ fun String.toUuidOrNull(): UUID? =
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* representations of genre fields into their named counterparts, and split up singular ID3v2-style
|
||||||
* ID3v2-style integer genre fields into one or more genres.
|
* integer genre fields into one or more genres.
|
||||||
* @param settings [Settings] required to obtain user separator configuration.
|
* @param settings [Settings] required to obtain user separator configuration.
|
||||||
* @return A list of one or more genre names..
|
* @return A list of one or more genre names..
|
||||||
*/
|
*/
|
||||||
|
@ -197,16 +194,15 @@ private fun String.parseId3v1Genre(): String? =
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Regex] that implements parsing for ID3v2's genre format.
|
* A [Regex] that implements parsing for ID3v2's genre format. Derived from mutagen:
|
||||||
* Derived from mutagen: https://github.com/quodlibet/mutagen
|
* https://github.com/quodlibet/mutagen
|
||||||
*/
|
*/
|
||||||
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
|
* Parse an ID3v2 integer genre field, which has support for multiple genre values and combined
|
||||||
* combined named/integer genres.
|
* named/integer genres.
|
||||||
* @return A list of one or more genres, or null if the field is not a valid ID3v2
|
* @return A list of one or more genres, or null if the field is not a valid ID3v2 integer genre.
|
||||||
* 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
|
||||||
|
|
|
@ -25,13 +25,12 @@ import com.google.android.material.checkbox.MaterialCheckBox
|
||||||
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.settings.Settings
|
||||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [ViewBindingDialogFragment] that allows the user to configure the separator characters
|
* A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to
|
||||||
* used to split tags with multiple values.
|
* split tags with multiple values. TODO: Add saved state for pending configurations.
|
||||||
* TODO: Add saved state for pending configurations.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
|
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
|
||||||
|
|
|
@ -57,8 +57,8 @@ class ArtistChoiceAdapter(private val listener: ClickableListListener) :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical
|
* A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [Artist] item, for
|
||||||
* [Artist] item, for use with [ArtistChoiceAdapter]. Use [new] to create an instance.
|
* use with [ArtistChoiceAdapter]. Use [new] to create an instance.
|
||||||
*/
|
*/
|
||||||
class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
|
class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
|
||||||
DialogRecyclerView.ViewHolder(binding.root) {
|
DialogRecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import androidx.navigation.fragment.navArgs
|
||||||
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [ArtistPickerDialog] intended for when [Artist] navigation is ambiguous.
|
* An [ArtistPickerDialog] intended for when [Artist] navigation is ambiguous.
|
||||||
|
|
|
@ -27,16 +27,17 @@ import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
||||||
import org.oxycblt.auxio.list.ClickableListListener
|
import org.oxycblt.auxio.list.ClickableListListener
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base class for dialogs that implements common behavior across all [Artist] pickers.
|
* The base class for dialogs that implements common behavior across all [Artist] pickers. These are
|
||||||
* These are shown whenever what to do with an item's [Artist] is ambiguous, as there are
|
* shown whenever what to do with an item's [Artist] is ambiguous, as there are multiple [Artist]'s
|
||||||
* multiple [Artist]'s to choose from.
|
* to choose from.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener {
|
abstract class ArtistPickerDialog :
|
||||||
|
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener {
|
||||||
protected val pickerModel: PickerViewModel by viewModels()
|
protected val pickerModel: PickerViewModel by viewModels()
|
||||||
// Okay to leak this since the Listener will not be called until after initialization.
|
// Okay to leak this since the Listener will not be called until after initialization.
|
||||||
private val artistAdapter = ArtistChoiceAdapter(@Suppress("LeakingThis") this)
|
private val artistAdapter = ArtistChoiceAdapter(@Suppress("LeakingThis") this)
|
||||||
|
|
|
@ -27,10 +27,9 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a [ViewModel] that manages the current music picker state.
|
* a [ViewModel] that manages the current music picker state. TODO: This really shouldn't exist.
|
||||||
* TODO: This really shouldn't exist. Make it so that the dialogs just contain the music
|
* Make it so that the dialogs just contain the music themselves and then exit if the library
|
||||||
* themselves and then exit if the library changes.
|
* changes. TODO: While we are at it, let's go and add ClickableSpan too to reduce the extent of
|
||||||
* TODO: While we are at it, let's go and add ClickableSpan too to reduce the extent of
|
|
||||||
* this dialog.
|
* this dialog.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
@ -46,7 +45,8 @@ class PickerViewModel : ViewModel(), MusicStore.Callback {
|
||||||
|
|
||||||
private val _currentArtists = MutableStateFlow<List<Artist>?>(null)
|
private val _currentArtists = MutableStateFlow<List<Artist>?>(null)
|
||||||
/**
|
/**
|
||||||
* The current [Artist] whose choices are being shown in the picker. Null/Empty if there is none.
|
* The current [Artist] whose choices are being shown in the picker. Null/Empty if there is
|
||||||
|
* none.
|
||||||
*/
|
*/
|
||||||
val currentArtists: StateFlow<List<Artist>?>
|
val currentArtists: StateFlow<List<Artist>?>
|
||||||
get() = _currentArtists
|
get() = _currentArtists
|
||||||
|
@ -90,5 +90,4 @@ class PickerViewModel : ViewModel(), MusicStore.Callback {
|
||||||
// Map the UIDs to artist instances and filter out the ones that can't be found.
|
// Map the UIDs to artist instances and filter out the ones that can't be found.
|
||||||
_currentArtists.value = uids.mapNotNull { library.find<Artist>(it) }.ifEmpty { null }
|
_currentArtists.value = uids.mapNotNull { library.find<Artist>(it) }.ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,8 @@ import org.oxycblt.auxio.util.inflater
|
||||||
* @param listener A [DirectoryAdapter.Listener] to bind interactions to.
|
* @param listener A [DirectoryAdapter.Listener] to bind interactions to.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class DirectoryAdapter(private val listener: Listener) : RecyclerView.Adapter<MusicDirViewHolder>() {
|
class DirectoryAdapter(private val listener: Listener) :
|
||||||
|
RecyclerView.Adapter<MusicDirViewHolder>() {
|
||||||
private val _dirs = mutableListOf<Directory>()
|
private val _dirs = mutableListOf<Directory>()
|
||||||
/**
|
/**
|
||||||
* The current list of [Directory]s, may not line up with [MusicDirectories] due to removals.
|
* The current list of [Directory]s, may not line up with [MusicDirectories] due to removals.
|
||||||
|
|
|
@ -29,7 +29,7 @@ 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.settings.Settings
|
||||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -58,8 +58,7 @@ class MusicDirsDialog :
|
||||||
.setNegativeButton(R.string.lbl_cancel, null)
|
.setNegativeButton(R.string.lbl_cancel, null)
|
||||||
.setPositiveButton(R.string.lbl_save) { _, _ ->
|
.setPositiveButton(R.string.lbl_save) { _, _ ->
|
||||||
val dirs = settings.getMusicDirs(storageManager)
|
val dirs = settings.getMusicDirs(storageManager)
|
||||||
val newDirs =
|
val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
|
||||||
MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
|
|
||||||
if (dirs != newDirs) {
|
if (dirs != newDirs) {
|
||||||
logD("Committing changes")
|
logD("Committing changes")
|
||||||
settings.setMusicDirs(newDirs)
|
settings.setMusicDirs(newDirs)
|
||||||
|
@ -69,7 +68,8 @@ class MusicDirsDialog :
|
||||||
|
|
||||||
override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) {
|
||||||
val launcher =
|
val launcher =
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs)
|
registerForActivityResult(
|
||||||
|
ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs)
|
||||||
|
|
||||||
// Now that the dialog exists, we get the view manually when the dialog is shown
|
// Now that the dialog exists, we get the view manually when the dialog is shown
|
||||||
// and override its click listener so that the dialog does not auto-dismiss when we
|
// and override its click listener so that the dialog does not auto-dismiss when we
|
||||||
|
@ -95,7 +95,9 @@ class MusicDirsDialog :
|
||||||
if (pendingDirs != null) {
|
if (pendingDirs != null) {
|
||||||
dirs =
|
dirs =
|
||||||
MusicDirectories(
|
MusicDirectories(
|
||||||
pendingDirs.mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) },
|
pendingDirs.mapNotNull {
|
||||||
|
Directory.fromDocumentTreeUri(storageManager, it)
|
||||||
|
},
|
||||||
savedInstanceState.getBoolean(KEY_PENDING_MODE))
|
savedInstanceState.getBoolean(KEY_PENDING_MODE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,9 +172,7 @@ class MusicDirsDialog :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Get if the UI has currently configured [MusicDirectories.shouldInclude] to be true. */
|
||||||
* Get if the UI has currently configured [MusicDirectories.shouldInclude] to be true.
|
|
||||||
*/
|
|
||||||
private fun isUiModeInclude(binding: DialogMusicDirsBinding) =
|
private fun isUiModeInclude(binding: DialogMusicDirsBinding) =
|
||||||
binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include
|
binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.storage
|
package org.oxycblt.auxio.music.storage
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -5,13 +22,12 @@ import android.os.storage.StorageManager
|
||||||
import android.os.storage.StorageVolume
|
import android.os.storage.StorageVolume
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import com.google.android.exoplayer2.util.MimeTypes
|
import com.google.android.exoplayer2.util.MimeTypes
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A full absolute path to a file. Only intended for display purposes. For accessing files,
|
* A full absolute path to a file. Only intended for display purposes. For accessing files, URIs are
|
||||||
* URIs are preferred in all cases due to scoped storage limitations.
|
* preferred in all cases due to scoped storage limitations.
|
||||||
* @param name The name of the file.
|
* @param name The name of the file.
|
||||||
* @param parent The parent [Directory] of the file.
|
* @param parent The parent [Directory] of the file.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
@ -35,12 +51,12 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
|
||||||
context.getString(R.string.fmt_path, volume.getDescriptionCompat(context), relativePath)
|
context.getString(R.string.fmt_path, volume.getDescriptionCompat(context), relativePath)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts this [Directory] instance into an opaque document tree path.
|
* Converts this [Directory] instance into an opaque document tree path. This is a huge
|
||||||
* This is a huge violation of the document tree URI contract, but it's also the only
|
* violation of the document tree URI contract, but it's also the only one can sensibly work
|
||||||
* one can sensibly work with these uris in the UI, and it doesn't exactly matter since
|
* with these uris in the UI, and it doesn't exactly matter since we never write or read
|
||||||
* we never write or read directory.
|
* directory.
|
||||||
* @return A URI [String] abiding by the document tree specification, or null
|
* @return A URI [String] abiding by the document tree specification, or null if the [Directory]
|
||||||
* if the [Directory] is not valid.
|
* is not valid.
|
||||||
*/
|
*/
|
||||||
fun toDocumentTreeUri() =
|
fun toDocumentTreeUri() =
|
||||||
// Document tree URIs consist of a prefixed volume name followed by a relative path.
|
// Document tree URIs consist of a prefixed volume name followed by a relative path.
|
||||||
|
@ -63,9 +79,7 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
|
||||||
other is Directory && other.volume == volume && other.relativePath == relativePath
|
other is Directory && other.volume == volume && other.relativePath == relativePath
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/** The name given to the internal volume when in a document tree URI. */
|
||||||
* The name given to the internal volume when in a document tree URI.
|
|
||||||
*/
|
|
||||||
private const val DOCUMENT_URI_PRIMARY_NAME = "primary"
|
private const val DOCUMENT_URI_PRIMARY_NAME = "primary"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,10 +94,9 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
|
||||||
volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator))
|
volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new directory from a document tree URI.
|
* Create a new directory from a document tree URI. This is a huge violation of the document
|
||||||
* This is a huge violation of the document tree URI contract, but it's also the only
|
* tree URI contract, but it's also the only one can sensibly work with these uris in the
|
||||||
* one can sensibly work with these uris in the UI, and it doesn't exactly matter since
|
* UI, and it doesn't exactly matter since we never write or read directory.
|
||||||
* we never write or read directory.
|
|
||||||
* @param storageManager [StorageManager] in order to obtain the [StorageVolume] specified
|
* @param storageManager [StorageManager] in order to obtain the [StorageVolume] specified
|
||||||
* in the given URI.
|
* in the given URI.
|
||||||
* @param uri The URI string to parse into a [Directory].
|
* @param uri The URI string to parse into a [Directory].
|
||||||
|
@ -109,12 +122,12 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the configuration for specific directories to filter to/from when loading music.
|
* Represents the configuration for specific directories to filter to/from when loading music. TODO:
|
||||||
* TODO: Migrate to a combined "Include + Exclude" system that is more sensible.
|
* Migrate to a combined "Include + Exclude" system that is more sensible.
|
||||||
* @param dirs A list of [Directory] instances. How these are interpreted depends on
|
* @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude]
|
||||||
* [shouldInclude].
|
* .
|
||||||
* @param shouldInclude True if the library should only load from the [Directory] instances,
|
* @param shouldInclude True if the library should only load from the [Directory] instances, false
|
||||||
* false if the library should not load from the [Directory] instances.
|
* if the library should not load from the [Directory] instances.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
data class MusicDirectories(val dirs: List<Directory>, val shouldInclude: Boolean)
|
data class MusicDirectories(val dirs: List<Directory>, val shouldInclude: Boolean)
|
||||||
|
@ -122,17 +135,17 @@ data class MusicDirectories(val dirs: List<Directory>, val shouldInclude: Boolea
|
||||||
/**
|
/**
|
||||||
* A mime type of a file. Only intended for display.
|
* A mime type of a file. Only intended for display.
|
||||||
* @param fromExtension The mime type obtained by analyzing the file extension.
|
* @param fromExtension The mime type obtained by analyzing the file extension.
|
||||||
* @param fromFormat The mime type obtained by analyzing the file format. Null if could
|
* @param fromFormat The mime type obtained by analyzing the file format. Null if could not be
|
||||||
* not be obtained.
|
* obtained.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
data class MimeType(val fromExtension: String, val fromFormat: String?) {
|
data class MimeType(val fromExtension: String, val fromFormat: String?) {
|
||||||
/**
|
/**
|
||||||
* Resolve the mime type into a human-readable format name, such as "Ogg Vorbis".
|
* Resolve the mime type into a human-readable format name, such as "Ogg Vorbis".
|
||||||
* @param context [Context] required to obtain human-readable strings.
|
* @param context [Context] required to obtain human-readable strings.
|
||||||
* @return A human-readable name for this mime type. Will first try [fromFormat],
|
* @return A human-readable name for this mime type. Will first try [fromFormat], then falling
|
||||||
* then falling back to [fromExtension], then falling back to the extension name,
|
* back to [fromExtension], then falling back to the extension name, and then finally a
|
||||||
* and then finally a placeholder "No Format" string.
|
* placeholder "No Format" string.
|
||||||
*/
|
*/
|
||||||
fun resolveName(context: Context): String {
|
fun resolveName(context: Context): String {
|
||||||
// We try our best to produce a more readable name for the common audio formats.
|
// We try our best to produce a more readable name for the common audio formats.
|
||||||
|
|
|
@ -34,8 +34,8 @@ import org.oxycblt.auxio.util.lazyReflectedMethod
|
||||||
// --- MEDIASTORE UTILITIES ---
|
// --- MEDIASTORE UTILITIES ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a content resolver that will not mangle MediaStore queries on certain devices.
|
* Get a content resolver that will not mangle MediaStore queries on certain devices. See
|
||||||
* See https://github.com/OxygenCobalt/Auxio/issues/50 for more info.
|
* https://github.com/OxygenCobalt/Auxio/issues/50 for more info.
|
||||||
*/
|
*/
|
||||||
val Context.contentResolverSafe: ContentResolver
|
val Context.contentResolverSafe: ContentResolver
|
||||||
get() = applicationContext.contentResolver
|
get() = applicationContext.contentResolver
|
||||||
|
@ -44,8 +44,8 @@ val Context.contentResolverSafe: ContentResolver
|
||||||
* A shortcut for querying the [ContentResolver] database.
|
* A shortcut for querying the [ContentResolver] database.
|
||||||
* @param uri The [Uri] of content to retrieve.
|
* @param uri The [Uri] of content to retrieve.
|
||||||
* @param projection A list of SQL columns to query from the database.
|
* @param projection A list of SQL columns to query from the database.
|
||||||
* @param selector A SQL selection statement to filter results. Spaces where
|
* @param selector A SQL selection statement to filter results. Spaces where arguments should be
|
||||||
* arguments should be filled in are represented with a "?".
|
* filled in are represented with a "?".
|
||||||
* @param args The arguments used for the selector.
|
* @param args The arguments used for the selector.
|
||||||
* @return A [Cursor] of the queried values, organized by the column projection.
|
* @return A [Cursor] of the queried values, organized by the column projection.
|
||||||
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
|
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
|
||||||
|
@ -56,20 +56,18 @@ fun ContentResolver.safeQuery(
|
||||||
projection: Array<out String>,
|
projection: Array<out String>,
|
||||||
selector: String? = null,
|
selector: String? = null,
|
||||||
args: Array<String>? = null
|
args: Array<String>? = null
|
||||||
) = requireNotNull(query(uri, projection, selector, args, null)) {
|
) = requireNotNull(query(uri, projection, selector, args, null)) { "ContentResolver query failed" }
|
||||||
"ContentResolver query failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A shortcut for [safeQuery] with [use] applied, automatically cleaning up the [Cursor]'s
|
* A shortcut for [safeQuery] with [use] applied, automatically cleaning up the [Cursor]'s resources
|
||||||
* resources when no longer used.
|
* when no longer used.
|
||||||
* @param uri The [Uri] of content to retrieve.
|
* @param uri The [Uri] of content to retrieve.
|
||||||
* @param projection A list of SQL columns to query from the database.
|
* @param projection A list of SQL columns to query from the database.
|
||||||
* @param selector A SQL selection statement to filter results. Spaces where
|
* @param selector A SQL selection statement to filter results. Spaces where arguments should be
|
||||||
* arguments should be filled in are represented with a "?".
|
* filled in are represented with a "?".
|
||||||
* @param args The arguments used for the selector.
|
* @param args The arguments used for the selector.
|
||||||
* @param block The block of code to run with the queried [Cursor]. Will not be ran if the
|
* @param block The block of code to run with the queried [Cursor]. Will not be ran if the [Cursor]
|
||||||
* [Cursor] is empty.
|
* is empty.
|
||||||
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
|
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
|
||||||
* @see ContentResolver.query
|
* @see ContentResolver.query
|
||||||
*/
|
*/
|
||||||
|
@ -81,9 +79,7 @@ inline fun <reified R> ContentResolver.useQuery(
|
||||||
block: (Cursor) -> R
|
block: (Cursor) -> R
|
||||||
) = safeQuery(uri, projection, selector, args).use(block)
|
) = safeQuery(uri, projection, selector, args).use(block)
|
||||||
|
|
||||||
/**
|
/** Album art [MediaStore] database is not a built-in constant, have to define it ourselves. */
|
||||||
* Album art [MediaStore] database is not a built-in constant, have to define it ourselves.
|
|
||||||
*/
|
|
||||||
private val EXTERNAL_COVERS_URI = Uri.parse("content://media/external/audio/albumart")
|
private val EXTERNAL_COVERS_URI = Uri.parse("content://media/external/audio/albumart")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -92,11 +88,12 @@ private val EXTERNAL_COVERS_URI = Uri.parse("content://media/external/audio/albu
|
||||||
* @see ContentUris.withAppendedId
|
* @see ContentUris.withAppendedId
|
||||||
* @see MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
* @see MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||||
*/
|
*/
|
||||||
fun Long.toAudioUri() = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this)
|
fun Long.toAudioUri() =
|
||||||
|
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a [MediaStore] Album ID into a [Uri] to it's system-provided album cover. This cover
|
* Convert a [MediaStore] Album ID into a [Uri] to it's system-provided album cover. This cover will
|
||||||
* will be fast to load, but will be lower quality.
|
* be fast to load, but will be lower quality.
|
||||||
* @return An external storage image [Uri]. May not exist.
|
* @return An external storage image [Uri]. May not exist.
|
||||||
* @see ContentUris.withAppendedId
|
* @see ContentUris.withAppendedId
|
||||||
*/
|
*/
|
||||||
|
@ -114,10 +111,9 @@ fun Long.toCoverUri() = ContentUris.withAppendedId(EXTERNAL_COVERS_URI, this)
|
||||||
private val SM_API21_GET_VOLUME_LIST_METHOD: Method by
|
private val SM_API21_GET_VOLUME_LIST_METHOD: Method by
|
||||||
lazyReflectedMethod(StorageManager::class, "getVolumeList")
|
lazyReflectedMethod(StorageManager::class, "getVolumeList")
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the analogous method to [StorageVolume.getDirectory] method that is usable from
|
* Provides the analogous method to [StorageVolume.getDirectory] method that is usable from API 21
|
||||||
* API 21 to API 23, in which the [StorageVolume] API was hidden and differed greatly.
|
* to API 23, in which the [StorageVolume] API was hidden and differed greatly.
|
||||||
* @see StorageVolume.getDirectory
|
* @see StorageVolume.getDirectory
|
||||||
*/
|
*/
|
||||||
@Suppress("NewApi")
|
@Suppress("NewApi")
|
||||||
|
@ -175,8 +171,8 @@ val StorageVolume.directoryCompat: String?
|
||||||
fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context)
|
fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this [StorageVolume] is considered the "Primary" volume where the Android System is
|
* If this [StorageVolume] is considered the "Primary" volume where the Android System is kept. May
|
||||||
* kept. May still be a removable volume.
|
* still be a removable volume.
|
||||||
* @see StorageVolume.isPrimary
|
* @see StorageVolume.isPrimary
|
||||||
*/
|
*/
|
||||||
val StorageVolume.isPrimaryCompat: Boolean
|
val StorageVolume.isPrimaryCompat: Boolean
|
||||||
|
@ -191,8 +187,8 @@ val StorageVolume.isEmulatedCompat: Boolean
|
||||||
@SuppressLint("NewApi") get() = isEmulated
|
@SuppressLint("NewApi") get() = isEmulated
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this [StorageVolume] represents the "Internal Shared Storage" volume, also known as
|
* If this [StorageVolume] represents the "Internal Shared Storage" volume, also known as "primary"
|
||||||
* "primary" to [MediaStore] and Document [Uri]s, obtained in a version compatible manner.
|
* to [MediaStore] and Document [Uri]s, obtained in a version compatible manner.
|
||||||
*/
|
*/
|
||||||
val StorageVolume.isInternalCompat: Boolean
|
val StorageVolume.isInternalCompat: Boolean
|
||||||
// Must contain the android system AND be an emulated drive, as non-emulated system
|
// Must contain the android system AND be an emulated drive, as non-emulated system
|
||||||
|
@ -200,24 +196,24 @@ val StorageVolume.isInternalCompat: Boolean
|
||||||
get() = isPrimaryCompat && isEmulatedCompat
|
get() = isPrimaryCompat && isEmulatedCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The unique identifier for this [StorageVolume], obtained in a version compatible manner
|
* The unique identifier for this [StorageVolume], obtained in a version compatible manner Can be
|
||||||
* Can be null.
|
* null.
|
||||||
* @see StorageVolume.getUuid
|
* @see StorageVolume.getUuid
|
||||||
*/
|
*/
|
||||||
val StorageVolume.uuidCompat: String?
|
val StorageVolume.uuidCompat: String?
|
||||||
@SuppressLint("NewApi") get() = uuid
|
@SuppressLint("NewApi") get() = uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current state of this [StorageVolume], such as "mounted" or "read-only", obtained in
|
* The current state of this [StorageVolume], such as "mounted" or "read-only", obtained in a
|
||||||
* a version compatible manner.
|
* version compatible manner.
|
||||||
* @see StorageVolume.getState
|
* @see StorageVolume.getState
|
||||||
*/
|
*/
|
||||||
val StorageVolume.stateCompat: String
|
val StorageVolume.stateCompat: String
|
||||||
@SuppressLint("NewApi") get() = state
|
@SuppressLint("NewApi") get() = state
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of this volume that can be used to interact with [MediaStore], in
|
* Returns the name of this volume that can be used to interact with [MediaStore], in a version
|
||||||
* a version compatible manner. Will be null if the volume is not scanned by [MediaStore].
|
* compatible manner. Will be null if the volume is not scanned by [MediaStore].
|
||||||
* @see StorageVolume.getMediaStoreVolumeName
|
* @see StorageVolume.getMediaStoreVolumeName
|
||||||
*/
|
*/
|
||||||
val StorageVolume.mediaStoreVolumeNameCompat: String?
|
val StorageVolume.mediaStoreVolumeNameCompat: String?
|
||||||
|
|
|
@ -43,10 +43,10 @@ import org.oxycblt.auxio.util.logW
|
||||||
/**
|
/**
|
||||||
* Core music loading state class.
|
* Core music loading state class.
|
||||||
*
|
*
|
||||||
* This class provides low-level access into the exact state of the music loading process.
|
* This class provides low-level access into the exact state of the music loading process. **This
|
||||||
* **This class should not be used in most cases.** It is highly volatile and provides far
|
* class should not be used in most cases.** It is highly volatile and provides far more information
|
||||||
* more information than is usually needed. Use [MusicStore] instead if you do not need to
|
* than is usually needed. Use [MusicStore] instead if you do not need to work with the exact music
|
||||||
* work with the exact music loading state.
|
* loading state.
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
@ -61,9 +61,9 @@ class Indexer private constructor() {
|
||||||
get() = indexingState != null
|
get() = indexingState != null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this instance has not completed a loading process and is not currently
|
* Whether this instance has not completed a loading process and is not currently loading music.
|
||||||
* loading music. This often occurs early in an app's lifecycle, and consumers should
|
* This often occurs early in an app's lifecycle, and consumers should try to avoid showing any
|
||||||
* try to avoid showing any state when this flag is true.
|
* state when this flag is true.
|
||||||
*/
|
*/
|
||||||
val isIndeterminate: Boolean
|
val isIndeterminate: Boolean
|
||||||
get() = lastResponse == null && indexingState == null
|
get() = lastResponse == null && indexingState == null
|
||||||
|
@ -105,9 +105,9 @@ class Indexer private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the [Callback] for this instance. This can be used to receive rapid-fire updates
|
* Register the [Callback] for this instance. This can be used to receive rapid-fire updates to
|
||||||
* to the current music loading state. There can be only one [Callback] at a time.
|
* the current music loading state. There can be only one [Callback] at a time. Will invoke all
|
||||||
* Will invoke all [Callback] methods to initialize the instance with the current state.
|
* [Callback] methods to initialize the instance with the current state.
|
||||||
* @param callback The [Callback] to add.
|
* @param callback The [Callback] to add.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
@ -125,10 +125,9 @@ class Indexer private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister a [Callback] from this instance, preventing it from recieving any further
|
* Unregister a [Callback] from this instance, preventing it from recieving any further updates.
|
||||||
* updates.
|
* @param callback The [Callback] to unregister. Must be the current [Callback]. Does nothing if
|
||||||
* @param callback The [Callback] to unregister. Must be the current [Callback]. Does
|
* invoked by another [Callback] implementation.
|
||||||
* nothing if invoked by another [Callback] implementation.
|
|
||||||
* @see Callback
|
* @see Callback
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
@ -145,8 +144,8 @@ class Indexer private constructor() {
|
||||||
* Start the indexing process. This should be done from in the background from [Controller]'s
|
* Start the indexing process. This should be done from in the background from [Controller]'s
|
||||||
* context after a command has been received to start the process.
|
* context after a command has been received to start the process.
|
||||||
* @param context [Context] required to load music.
|
* @param context [Context] required to load music.
|
||||||
* @param withCache Whether to use the cache or not when loading. If false, the cache will
|
* @param withCache Whether to use the cache or not when loading. If false, the cache will still
|
||||||
* still be written, but no cache entries will be loaded into the new library.
|
* be written, but no cache entries will be loaded into the new library.
|
||||||
*/
|
*/
|
||||||
suspend fun index(context: Context, withCache: Boolean) {
|
suspend fun index(context: Context, withCache: Boolean) {
|
||||||
if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
|
if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
|
||||||
|
@ -186,9 +185,9 @@ class Indexer private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request that the music library should be reloaded. This should be used by components that
|
* Request that the music library should be reloaded. This should be used by components that do
|
||||||
* do not manage the indexing process in order to signal that the [Controller] should call
|
* not manage the indexing process in order to signal that the [Controller] should call [index]
|
||||||
* [index] eventually.
|
* eventually.
|
||||||
* @param withCache Whether to use the cache when loading music. Does nothing if there is no
|
* @param withCache Whether to use the cache when loading music. Does nothing if there is no
|
||||||
* [Controller].
|
* [Controller].
|
||||||
*/
|
*/
|
||||||
|
@ -199,8 +198,8 @@ class Indexer private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the current loading state to signal that the instance is not loading. This should
|
* Reset the current loading state to signal that the instance is not loading. This should be
|
||||||
* be called by [Controller] after it's indexing co-routine was cancelled.
|
* called by [Controller] after it's indexing co-routine was cancelled.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun reset() {
|
fun reset() {
|
||||||
|
@ -211,15 +210,16 @@ class Indexer private constructor() {
|
||||||
/**
|
/**
|
||||||
* Internal implementation of the music loading process.
|
* Internal implementation of the music loading process.
|
||||||
* @param context [Context] required to load music.
|
* @param context [Context] required to load music.
|
||||||
* @param withCache Whether to use the cache or not when loading. If false, the cache will
|
* @param withCache Whether to use the cache or not when loading. If false, the cache will still
|
||||||
* still be written, but no cache entries will be loaded into the new library.
|
* be written, but no cache entries will be loaded into the new library.
|
||||||
* @return A newly-loaded [MusicStore.Library], or null if nothing was loaded.
|
* @return A newly-loaded [MusicStore.Library], or null if nothing was loaded.
|
||||||
*/
|
*/
|
||||||
private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library? {
|
private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library? {
|
||||||
// Create the chain of extractors. Each extractor builds on the previous and
|
// Create the chain of extractors. Each extractor builds on the previous and
|
||||||
// enables version-specific features in order to create the best possible music
|
// enables version-specific features in order to create the best possible music
|
||||||
// experience.
|
// experience.
|
||||||
val cacheDatabase = if (withCache) {
|
val cacheDatabase =
|
||||||
|
if (withCache) {
|
||||||
ReadWriteCacheExtractor(context)
|
ReadWriteCacheExtractor(context)
|
||||||
} else {
|
} else {
|
||||||
WriteOnlyCacheExtractor(context)
|
WriteOnlyCacheExtractor(context)
|
||||||
|
@ -255,11 +255,11 @@ class Indexer private constructor() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a list of [Song]s from the device.
|
* Load a list of [Song]s from the device.
|
||||||
* @param metadataExtractor The completed [MetadataExtractor] instance to use to load
|
* @param metadataExtractor The completed [MetadataExtractor] instance to use to load [Song.Raw]
|
||||||
* [Song.Raw] instances.
|
* instances.
|
||||||
* @param settings [Settings] required to create [Song] instances.
|
* @param settings [Settings] required to create [Song] instances.
|
||||||
* @return A possibly empty list of [Song]s. These [Song]s will be incomplete and
|
* @return A possibly empty list of [Song]s. These [Song]s will be incomplete and must be linked
|
||||||
* must be linked with parent [Album], [Artist], and [Genre] items in order to be usable.
|
* with parent [Album], [Artist], and [Genre] items in order to be usable.
|
||||||
*/
|
*/
|
||||||
private suspend fun buildSongs(
|
private suspend fun buildSongs(
|
||||||
metadataExtractor: MetadataExtractor,
|
metadataExtractor: MetadataExtractor,
|
||||||
|
@ -301,10 +301,10 @@ class Indexer private constructor() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a list of [Album]s from the given [Song]s.
|
* Build a list of [Album]s from the given [Song]s.
|
||||||
* @param songs The [Song]s to build [Album]s from. These will be linked with their
|
* @param songs The [Song]s to build [Album]s from. These will be linked with their respective
|
||||||
* respective [Album]s when created.
|
* [Album]s when created.
|
||||||
* @return A non-empty list of [Album]s. These [Album]s will be incomplete and
|
* @return A non-empty list of [Album]s. These [Album]s will be incomplete and must be linked
|
||||||
* must be linked with parent [Artist] instances in order to be usable.
|
* with parent [Artist] instances in order to be usable.
|
||||||
*/
|
*/
|
||||||
private fun buildAlbums(songs: List<Song>): List<Album> {
|
private fun buildAlbums(songs: List<Song>): List<Album> {
|
||||||
// Group songs by their singular raw album, then map the raw instances and their
|
// Group songs by their singular raw album, then map the raw instances and their
|
||||||
|
@ -316,17 +316,17 @@ class Indexer private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required
|
* Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required as
|
||||||
* as they group into [Artist] instances much differently, with [Song]s being grouped
|
* they group into [Artist] instances much differently, with [Song]s being grouped primarily by
|
||||||
* primarily by artist names, and [Album]s being grouped primarily by album artist names.
|
* artist names, and [Album]s being grouped primarily by album artist names.
|
||||||
* @param songs The [Song]s to build [Artist]s from. One [Song] can result in
|
* @param songs The [Song]s to build [Artist]s from. One [Song] can result in the creation of
|
||||||
* the creation of one or more [Artist] instances. These will be linked with their
|
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
|
||||||
* respective [Artist]s when created.
|
* created.
|
||||||
* @param albums The [Album]s to build [Artist]s from. One [Album] can result in
|
* @param albums The [Album]s to build [Artist]s from. One [Album] can result in the creation of
|
||||||
* the creation of one or more [Artist] instances. These will be linked with their
|
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
|
||||||
* respective [Artist]s when created.
|
* created.
|
||||||
* @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined
|
* @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined groupings
|
||||||
* groupings of [Song]s and [Album]s.
|
* of [Song]s and [Album]s.
|
||||||
*/
|
*/
|
||||||
private fun buildArtists(songs: List<Song>, albums: List<Album>): List<Artist> {
|
private fun buildArtists(songs: List<Song>, albums: List<Album>): List<Artist> {
|
||||||
// Add every raw artist credited to each Song/Album to the grouping. This way,
|
// Add every raw artist credited to each Song/Album to the grouping. This way,
|
||||||
|
@ -353,9 +353,9 @@ class Indexer private constructor() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group up [Song]s into [Genre] instances.
|
* Group up [Song]s into [Genre] instances.
|
||||||
* @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in
|
* @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in the creation of
|
||||||
* the creation of one or more [Genre] instances. These will be linked with their
|
* one or more [Genre] instances. These will be linked with their respective [Genre]s when
|
||||||
* respective [Genre]s when created.
|
* created.
|
||||||
* @return A non-empty list of [Genre]s.
|
* @return A non-empty list of [Genre]s.
|
||||||
*/
|
*/
|
||||||
private fun buildGenres(songs: List<Song>): List<Genre> {
|
private fun buildGenres(songs: List<Song>): List<Genre> {
|
||||||
|
@ -376,8 +376,8 @@ class Indexer private constructor() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit a new [State.Indexing] state. This can be used to signal the current state of the music
|
* Emit a new [State.Indexing] state. This can be used to signal the current state of the music
|
||||||
* loading process to external code. Assumes that the callee has already checked if they
|
* loading process to external code. Assumes that the callee has already checked if they have
|
||||||
* have not been canceled and thus have the ability to emit a new state.
|
* not been canceled and thus have the ability to emit a new state.
|
||||||
* @param indexing The new [Indexing] state to emit, or null if no loading process is occurring.
|
* @param indexing The new [Indexing] state to emit, or null if no loading process is occurring.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
@ -393,8 +393,8 @@ class Indexer private constructor() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit a new [State.Complete] state. This can be used to signal the completion of the music
|
* Emit a new [State.Complete] state. This can be used to signal the completion of the music
|
||||||
* loading process to external code. Will check if the callee has not been canceled and thus
|
* loading process to external code. Will check if the callee has not been canceled and thus has
|
||||||
* has the ability to emit a new state
|
* the ability to emit a new state
|
||||||
* @param response The new [Response] to emit, representing the outcome of the music loading
|
* @param response The new [Response] to emit, representing the outcome of the music loading
|
||||||
* process.
|
* process.
|
||||||
*/
|
*/
|
||||||
|
@ -439,8 +439,7 @@ class Indexer private constructor() {
|
||||||
*/
|
*/
|
||||||
sealed class Indexing {
|
sealed class Indexing {
|
||||||
/**
|
/**
|
||||||
* Music loading is occurring, but no definite estimate can be put on the current
|
* Music loading is occurring, but no definite estimate can be put on the current progress.
|
||||||
* progress.
|
|
||||||
*/
|
*/
|
||||||
object Indeterminate : Indexing()
|
object Indeterminate : Indexing()
|
||||||
|
|
||||||
|
@ -477,8 +476,8 @@ class Indexer private constructor() {
|
||||||
* A callback for rapid-fire changes in the music loading state.
|
* A callback for rapid-fire changes in the music loading state.
|
||||||
*
|
*
|
||||||
* This is only useful for code that absolutely must show the current loading process.
|
* This is only useful for code that absolutely must show the current loading process.
|
||||||
* Otherwise, [MusicStore.Callback] is highly recommended due to it's updates only
|
* Otherwise, [MusicStore.Callback] is highly recommended due to it's updates only consisting of
|
||||||
* consisting of the [MusicStore.Library].
|
* the [MusicStore.Library].
|
||||||
*/
|
*/
|
||||||
interface Callback {
|
interface Callback {
|
||||||
/**
|
/**
|
||||||
|
@ -493,13 +492,13 @@ class Indexer private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context that runs the music loading process. Implementations should be capable of
|
* Context that runs the music loading process. Implementations should be capable of running the
|
||||||
* running the background for long periods of time without android killing the process.
|
* background for long periods of time without android killing the process.
|
||||||
*/
|
*/
|
||||||
interface Controller : Callback {
|
interface Controller : Callback {
|
||||||
/**
|
/**
|
||||||
* Called when a new music loading process was requested. Implementations should
|
* Called when a new music loading process was requested. Implementations should forward
|
||||||
* forward this to [index].
|
* this to [index].
|
||||||
* @param withCache Whether to use the cache or not when loading. If false, the cache should
|
* @param withCache Whether to use the cache or not when loading. If false, the cache should
|
||||||
* still be written, but no cache entries will be loaded into the new library.
|
* still be written, but no cache entries will be loaded into the new library.
|
||||||
* @see index
|
* @see index
|
||||||
|
@ -511,9 +510,8 @@ class Indexer private constructor() {
|
||||||
@Volatile private var INSTANCE: Indexer? = null
|
@Volatile private var INSTANCE: Indexer? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A version-compatible identifier for the read external storage permission required
|
* A version-compatible identifier for the read external storage permission required by the
|
||||||
* by the system to load audio.
|
* system to load audio. TODO: Move elsewhere.
|
||||||
* TODO: Move elsewhere.
|
|
||||||
*/
|
*/
|
||||||
val PERMISSION_READ_AUDIO =
|
val PERMISSION_READ_AUDIO =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import androidx.core.app.NotificationCompat
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.shared.ForegroundServiceNotification
|
import org.oxycblt.auxio.service.ForegroundServiceNotification
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||||
|
|
||||||
|
@ -89,11 +89,12 @@ class IndexingNotification(private val context: Context) :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A static [ForegroundServiceNotification] that signals to the user that the app is currently monitoring
|
* A static [ForegroundServiceNotification] that signals to the user that the app is currently
|
||||||
* the music library for changes.
|
* monitoring the music library for changes.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class ObservingNotification(context: Context) : ForegroundServiceNotification(context, INDEXER_CHANNEL) {
|
class ObservingNotification(context: Context) :
|
||||||
|
ForegroundServiceNotification(context, INDEXER_CHANNEL) {
|
||||||
init {
|
init {
|
||||||
setSmallIcon(R.drawable.ic_indexer_24)
|
setSmallIcon(R.drawable.ic_indexer_24)
|
||||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
|
|
|
@ -35,20 +35,20 @@ import org.oxycblt.auxio.R
|
||||||
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.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.shared.ForegroundManager
|
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Service] that manages the background music loading process.
|
* A [Service] that manages the background music loading process.
|
||||||
*
|
*
|
||||||
* Loading music is a time-consuming process that would likely be killed by the system before
|
* Loading music is a time-consuming process that would likely be killed by the system before it
|
||||||
* it could complete if ran anywhere else. So, this [Service] manages the music loading process
|
* could complete if ran anywhere else. So, this [Service] manages the music loading process as an
|
||||||
* as an instance of [Indexer.Controller].
|
* instance of [Indexer.Controller].
|
||||||
*
|
*
|
||||||
* This [Service] also handles automatic rescanning, as that is a similarly long-running
|
* This [Service] also handles automatic rescanning, as that is a similarly long-running background
|
||||||
* background operation that would be unsuitable elsewhere in the app.
|
* operation that would be unsuitable elsewhere in the app.
|
||||||
*
|
*
|
||||||
* TODO: Unify with PlaybackService as part of the service independence project
|
* TODO: Unify with PlaybackService as part of the service independence project
|
||||||
*
|
*
|
||||||
|
@ -121,8 +121,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
indexer.reset()
|
indexer.reset()
|
||||||
}
|
}
|
||||||
// Start a new music loading job on a co-routine.
|
// Start a new music loading job on a co-routine.
|
||||||
currentIndexJob = indexScope.launch {
|
currentIndexJob = indexScope.launch { indexer.index(this@IndexerService, withCache) }
|
||||||
indexer.index(this@IndexerService, withCache) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIndexerStateChanged(state: Indexer.State?) {
|
override fun onIndexerStateChanged(state: Indexer.State?) {
|
||||||
|
@ -165,8 +164,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
// --- INTERNAL ---
|
// --- INTERNAL ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current state to "Active", in which the service signals that music
|
* Update the current state to "Active", in which the service signals that music loading is
|
||||||
* loading is on-going.
|
* on-going.
|
||||||
* @param state The current music loading state.
|
* @param state The current music loading state.
|
||||||
*/
|
*/
|
||||||
private fun updateActiveSession(state: Indexer.Indexing) {
|
private fun updateActiveSession(state: Indexer.Indexing) {
|
||||||
|
@ -184,8 +183,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current state to "Idle", in which it either does nothing or signals
|
* Update the current state to "Idle", in which it either does nothing or signals that it's
|
||||||
* that it's currently monitoring the music library for changes.
|
* currently monitoring the music library for changes.
|
||||||
*/
|
*/
|
||||||
private fun updateIdleSession() {
|
private fun updateIdleSession() {
|
||||||
if (settings.shouldBeObserving) {
|
if (settings.shouldBeObserving) {
|
||||||
|
@ -208,9 +207,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
wakeLock.releaseSafe()
|
wakeLock.releaseSafe()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Utility to safely acquire a [PowerManager.WakeLock] without crashes/inefficiency. */
|
||||||
* Utility to safely acquire a [PowerManager.WakeLock] without crashes/inefficiency.
|
|
||||||
*/
|
|
||||||
private fun PowerManager.WakeLock.acquireSafe() {
|
private fun PowerManager.WakeLock.acquireSafe() {
|
||||||
// Avoid unnecessary acquire calls.
|
// Avoid unnecessary acquire calls.
|
||||||
if (!wakeLock.isHeld) {
|
if (!wakeLock.isHeld) {
|
||||||
|
@ -222,9 +219,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Utility to safely release a [PowerManager.WakeLock] without crashes/inefficiency. */
|
||||||
* Utility to safely release a [PowerManager.WakeLock] without crashes/inefficiency.
|
|
||||||
*/
|
|
||||||
private fun PowerManager.WakeLock.releaseSafe() {
|
private fun PowerManager.WakeLock.releaseSafe() {
|
||||||
// Avoid unnecessary release calls.
|
// Avoid unnecessary release calls.
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
|
@ -259,7 +254,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
* known to the user as automatic rescanning. The active (and not passive) nature of observing
|
* known to the user as automatic rescanning. The active (and not passive) nature of observing
|
||||||
* the database is what requires [IndexerService] to stay foreground when this is enabled.
|
* the database is what requires [IndexerService] to stay foreground when this is enabled.
|
||||||
*/
|
*/
|
||||||
private inner class SystemContentObserver : ContentObserver(Handler(Looper.getMainLooper())), Runnable {
|
private inner class SystemContentObserver :
|
||||||
|
ContentObserver(Handler(Looper.getMainLooper())), Runnable {
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -25,9 +25,9 @@ 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.settings.Settings
|
||||||
import org.oxycblt.auxio.shared.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||||
|
|
|
@ -33,9 +33,9 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
||||||
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.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.shared.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
|
|
@ -41,8 +41,7 @@ import org.oxycblt.auxio.util.inflater
|
||||||
* @param listener A [Listener] to bind interactions to.
|
* @param listener A [Listener] to bind interactions to.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class QueueAdapter(private val listener: Listener) :
|
class QueueAdapter(private val listener: Listener) : RecyclerView.Adapter<QueueSongViewHolder>() {
|
||||||
RecyclerView.Adapter<QueueSongViewHolder>() {
|
|
||||||
private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK)
|
private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK)
|
||||||
// Since PlayingIndicator adapter relies on an item value, we cannot use it for this
|
// Since PlayingIndicator adapter relies on an item value, we cannot use it for this
|
||||||
// adapter, as one item can appear at several points in the UI. Use a similar implementation
|
// adapter, as one item can appear at several points in the UI. Use a similar implementation
|
||||||
|
@ -72,8 +71,8 @@ class QueueAdapter(private val listener: Listener) :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronously update the list with new items. This is exceedingly slow for large diffs,
|
* Synchronously update the list with new items. This is exceedingly slow for large diffs, so
|
||||||
* so only use it for trivial updates.
|
* only use it for trivial updates.
|
||||||
* @param newList The new [Song]s for the adapter to display.
|
* @param newList The new [Song]s for the adapter to display.
|
||||||
*/
|
*/
|
||||||
fun submitList(newList: List<Song>) {
|
fun submitList(newList: List<Song>) {
|
||||||
|
@ -81,8 +80,8 @@ class QueueAdapter(private val listener: Listener) :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the list with a new list. This is exceedingly slow for large diffs,
|
* Replace the list with a new list. This is exceedingly slow for large diffs, so only use it
|
||||||
* so only use it for trivial updates.
|
* for trivial updates.
|
||||||
* @param newList The new [Song]s for the adapter to display.
|
* @param newList The new [Song]s for the adapter to display.
|
||||||
*/
|
*/
|
||||||
fun replaceList(newList: List<Song>) {
|
fun replaceList(newList: List<Song>) {
|
||||||
|
@ -90,8 +89,8 @@ class QueueAdapter(private val listener: Listener) :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the position of the currently playing item in the queue. This will mark the item
|
* Set the position of the currently playing item in the queue. This will mark the item as
|
||||||
* as playing and any previous items as played.
|
* playing and any previous items as played.
|
||||||
* @param index The position of the currently playing item in the queue.
|
* @param index The position of the currently playing item in the queue.
|
||||||
* @param isPlaying Whether playback is ongoing or paused.
|
* @param isPlaying Whether playback is ongoing or paused.
|
||||||
*/
|
*/
|
||||||
|
@ -122,9 +121,7 @@ class QueueAdapter(private val listener: Listener) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A listener for queue list events. */
|
||||||
* A listener for queue list events.
|
|
||||||
*/
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
/**
|
/**
|
||||||
* Called when a [RecyclerView.ViewHolder] in the list as clicked.
|
* Called when a [RecyclerView.ViewHolder] in the list as clicked.
|
||||||
|
@ -152,21 +149,15 @@ class QueueAdapter(private val listener: Listener) :
|
||||||
*/
|
*/
|
||||||
class QueueSongViewHolder private constructor(private val binding: ItemQueueSongBinding) :
|
class QueueSongViewHolder private constructor(private val binding: ItemQueueSongBinding) :
|
||||||
PlayingIndicatorAdapter.ViewHolder(binding.root) {
|
PlayingIndicatorAdapter.ViewHolder(binding.root) {
|
||||||
/**
|
/** The "body" view of this [QueueSongViewHolder] that shows the [Song] information. */
|
||||||
* The "body" view of this [QueueSongViewHolder] that shows the [Song] information.
|
|
||||||
*/
|
|
||||||
val bodyView: View
|
val bodyView: View
|
||||||
get() = binding.body
|
get() = binding.body
|
||||||
|
|
||||||
/**
|
/** The background view of this [QueueSongViewHolder] that shows the delete icon. */
|
||||||
* The background view of this [QueueSongViewHolder] that shows the delete icon.
|
|
||||||
*/
|
|
||||||
val backgroundView: View
|
val backgroundView: View
|
||||||
get() = binding.background
|
get() = binding.background
|
||||||
|
|
||||||
/**
|
/** The actual background drawable of this [QueueSongViewHolder] that can be manipulated. */
|
||||||
* The actual background drawable of this [QueueSongViewHolder] that can be manipulated.
|
|
||||||
*/
|
|
||||||
val backgroundDrawable =
|
val backgroundDrawable =
|
||||||
MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply {
|
MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply {
|
||||||
fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
|
fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
|
||||||
|
@ -174,9 +165,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
|
||||||
alpha = 0
|
alpha = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** If this queue item is considered "in the future" (i.e has not played yet). */
|
||||||
* If this queue item is considered "in the future" (i.e has not played yet).
|
|
||||||
*/
|
|
||||||
var isFuture: Boolean
|
var isFuture: Boolean
|
||||||
get() = binding.songAlbumCover.isEnabled
|
get() = binding.songAlbumCover.isEnabled
|
||||||
set(value) {
|
set(value) {
|
||||||
|
@ -205,9 +194,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong
|
||||||
*/
|
*/
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
fun bind(song: Song, listener: QueueAdapter.Listener) {
|
fun bind(song: Song, listener: QueueAdapter.Listener) {
|
||||||
binding.body.setOnClickListener {
|
binding.body.setOnClickListener { listener.onClick(this) }
|
||||||
listener.onClick(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.songAlbumCover.bind(song)
|
binding.songAlbumCover.bind(song)
|
||||||
binding.songName.text = song.resolveName(binding.context)
|
binding.songName.text = song.resolveName(binding.context)
|
||||||
|
|
|
@ -28,8 +28,8 @@ import org.oxycblt.auxio.util.getInteger
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A highly customized [ItemTouchHelper.Callback] that enables some extra eye candy in the queue
|
* A highly customized [ItemTouchHelper.Callback] that enables some extra eye candy in the queue UI,
|
||||||
* UI, such as an animation when lifting items.
|
* such as an animation when lifting items.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHelper.Callback() {
|
class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHelper.Callback() {
|
||||||
|
@ -73,7 +73,8 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
|
||||||
holder.itemView
|
holder.itemView
|
||||||
.animate()
|
.animate()
|
||||||
.translationZ(elevation)
|
.translationZ(elevation)
|
||||||
.setDuration(recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong())
|
.setDuration(
|
||||||
|
recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong())
|
||||||
.setUpdateListener {
|
.setUpdateListener {
|
||||||
bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt()
|
bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt()
|
||||||
}
|
}
|
||||||
|
@ -114,7 +115,8 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
|
||||||
holder.itemView
|
holder.itemView
|
||||||
.animate()
|
.animate()
|
||||||
.translationZ(0f)
|
.translationZ(0f)
|
||||||
.setDuration(recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong())
|
.setDuration(
|
||||||
|
recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong())
|
||||||
.setUpdateListener {
|
.setUpdateListener {
|
||||||
bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt()
|
bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt()
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
|
@ -52,8 +52,8 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start playing the the queue item at the given index.
|
* Start playing the the queue item at the given index.
|
||||||
* @param adapterIndex The index of the queue item to play. Does nothing if the index is out
|
* @param adapterIndex The index of the queue item to play. Does nothing if the index is out of
|
||||||
* of range.
|
* range.
|
||||||
*/
|
*/
|
||||||
fun goto(adapterIndex: Int) {
|
fun goto(adapterIndex: Int) {
|
||||||
if (adapterIndex !in playbackManager.queue.indices) {
|
if (adapterIndex !in playbackManager.queue.indices) {
|
||||||
|
@ -65,8 +65,8 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a queue item at the given index.
|
* Remove a queue item at the given index.
|
||||||
* @param adapterIndex The index of the queue item to play. Does nothing if the index is
|
* @param adapterIndex The index of the queue item to play. Does nothing if the index is out of
|
||||||
* out of range.
|
* range.
|
||||||
*/
|
*/
|
||||||
fun removeQueueDataItem(adapterIndex: Int) {
|
fun removeQueueDataItem(adapterIndex: Int) {
|
||||||
if (adapterIndex <= playbackManager.index ||
|
if (adapterIndex <= playbackManager.index ||
|
||||||
|
@ -93,16 +93,12 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Finish a replace flag specified by [replaceQueue]. */
|
||||||
* Finish a replace flag specified by [replaceQueue].
|
|
||||||
*/
|
|
||||||
fun finishReplace() {
|
fun finishReplace() {
|
||||||
replaceQueue = null
|
replaceQueue = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Finish a scroll operation started by [scrollTo]. */
|
||||||
* Finish a scroll operation started by [scrollTo].
|
|
||||||
*/
|
|
||||||
fun finishScrollTo() {
|
fun finishScrollTo() {
|
||||||
scrollTo = null
|
scrollTo = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ 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.settings.Settings
|
||||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -29,7 +29,7 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
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.shared.ForegroundServiceNotification
|
import org.oxycblt.auxio.service.ForegroundServiceNotification
|
||||||
import org.oxycblt.auxio.util.newBroadcastPendingIntent
|
import org.oxycblt.auxio.util.newBroadcastPendingIntent
|
||||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||||
|
|
||||||
|
|
|
@ -54,8 +54,8 @@ 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.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.shared.ForegroundManager
|
|
||||||
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
|
||||||
|
|
|
@ -51,8 +51,8 @@ abstract class BaseBottomSheetBehavior<V : View>(context: Context, attributeSet:
|
||||||
abstract fun createBackground(context: Context): Drawable
|
abstract fun createBackground(context: Context): Drawable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when window insets are being applied to the [View] this [BaseBottomSheetBehavior]
|
* Called when window insets are being applied to the [View] this [BaseBottomSheetBehavior] is
|
||||||
* is linked to.
|
* linked to.
|
||||||
* @param child The child view recieving the [WindowInsets].
|
* @param child The child view recieving the [WindowInsets].
|
||||||
* @param insets The [WindowInsets] to apply.
|
* @param insets The [WindowInsets] to apply.
|
||||||
* @return The (possibly modified) [WindowInsets].
|
* @return The (possibly modified) [WindowInsets].
|
||||||
|
|
|
@ -72,8 +72,8 @@ class SearchAdapter(private val listener: SelectableListListener) :
|
||||||
override fun isItemFullWidth(position: Int) = differ.currentList[position] is Header
|
override fun isItemFullWidth(position: Int) = differ.currentList[position] is Header
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronously update the list with new items. Assumes that the list only contains
|
* Asynchronously update the list with new items. Assumes that the list only contains supported
|
||||||
* supported data..
|
* data..
|
||||||
* @param newList The new [Item]s for the adapter to display.
|
* @param newList The new [Item]s for the adapter to display.
|
||||||
* @param callback A block called when the asynchronous update is completed.
|
* @param callback A block called when the asynchronous update is completed.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -208,9 +208,7 @@ class SearchFragment : ListFragment<FragmentSearchBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Safely hide the keyboard from this view. */
|
||||||
* Safely hide the keyboard from this view.
|
|
||||||
*/
|
|
||||||
private fun InputMethodManager.hide() {
|
private fun InputMethodManager.hide() {
|
||||||
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,9 @@ import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.R
|
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.Album
|
|
||||||
import org.oxycblt.auxio.music.Artist
|
|
||||||
import org.oxycblt.auxio.music.Genre
|
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
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.settings.Settings
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
|
@ -129,7 +125,9 @@ class SearchViewModel(application: Application) :
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterMode == null || filterMode == MusicMode.SONGS) {
|
if (filterMode == null || filterMode == MusicMode.SONGS) {
|
||||||
library.songs.searchListImpl(query) { q, song -> song.path.name.contains(q) }?.let {
|
library.songs
|
||||||
|
.searchListImpl(query) { q, song -> song.path.name.contains(q) }
|
||||||
|
?.let {
|
||||||
results.add(Header(R.string.lbl_songs))
|
results.add(Header(R.string.lbl_songs))
|
||||||
results.addAll(sort.songs(it))
|
results.addAll(sort.songs(it))
|
||||||
}
|
}
|
||||||
|
@ -141,16 +139,17 @@ class SearchViewModel(application: Application) :
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search a given [Music] list.
|
* Search a given [Music] list.
|
||||||
* @param query The query to search for. The routine will compare this query to the names
|
* @param query The query to search for. The routine will compare this query to the names of
|
||||||
* of each object in the list and
|
* each object in the list and
|
||||||
* @param fallback Additional comparison code to run if the item does not match the query
|
* @param fallback Additional comparison code to run if the item does not match the query
|
||||||
* initially. This can be used to compare against additional attributes to improve search
|
* initially. This can be used to compare against additional attributes to improve search result
|
||||||
* result quality.
|
* quality.
|
||||||
*/
|
*/
|
||||||
private inline fun <T : Music> List<T>.searchListImpl(
|
private inline fun <T : Music> List<T>.searchListImpl(
|
||||||
query: String,
|
query: String,
|
||||||
fallback: (String, T) -> Boolean = { _, _ -> false }
|
fallback: (String, T) -> Boolean = { _, _ -> false }
|
||||||
) = filter {
|
) =
|
||||||
|
filter {
|
||||||
// See if the plain resolved name matches the query. This works for most situations.
|
// See if the plain resolved name matches the query. This works for most situations.
|
||||||
val name = it.resolveName(context)
|
val name = it.resolveName(context)
|
||||||
if (name.contains(query, ignoreCase = true)) {
|
if (name.contains(query, ignoreCase = true)) {
|
||||||
|
@ -167,7 +166,8 @@ class SearchViewModel(application: Application) :
|
||||||
// As a last-ditch effort, see if the normalized name matches. This will replace
|
// As a last-ditch effort, see if the normalized name matches. This will replace
|
||||||
// any non-alphabetical characters with their alphabetical representations, which
|
// any non-alphabetical characters with their alphabetical representations, which
|
||||||
// could make it match the query.
|
// could make it match the query.
|
||||||
val normalizedName = NORMALIZATION_SANITIZE_REGEX.replace(
|
val normalizedName =
|
||||||
|
NORMALIZATION_SANITIZE_REGEX.replace(
|
||||||
Normalizer.normalize(name, Normalizer.Form.NFKD), "")
|
Normalizer.normalize(name, Normalizer.Form.NFKD), "")
|
||||||
if (normalizedName.contains(query, ignoreCase = true)) {
|
if (normalizedName.contains(query, ignoreCase = true)) {
|
||||||
return@filter true
|
return@filter true
|
||||||
|
@ -212,7 +212,6 @@ class SearchViewModel(application: Application) :
|
||||||
search(lastQuery)
|
search(lastQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Converts the output of [Normalizer] to remove any junk characters added by it's
|
* Converts the output of [Normalizer] to remove any junk characters added by it's
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.shared
|
package org.oxycblt.auxio.service
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
|
@ -31,17 +31,15 @@ import org.oxycblt.auxio.util.logD
|
||||||
class ForegroundManager(private val service: Service) {
|
class ForegroundManager(private val service: Service) {
|
||||||
private var isForeground = false
|
private var isForeground = false
|
||||||
|
|
||||||
/**
|
/** Release this instance. */
|
||||||
* Release this instance.
|
|
||||||
*/
|
|
||||||
fun release() {
|
fun release() {
|
||||||
tryStopForeground()
|
tryStopForeground()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to enter a foreground state.
|
* Try to enter a foreground state.
|
||||||
* @param notification The [ForegroundServiceNotification] to show in order to signal the foreground
|
* @param notification The [ForegroundServiceNotification] to show in order to signal the
|
||||||
* state.
|
* foreground state.
|
||||||
* @return true if the state was changed, false otherwise
|
* @return true if the state was changed, false otherwise
|
||||||
* @see Service.startForeground
|
* @see Service.startForeground
|
||||||
*/
|
*/
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.shared
|
package org.oxycblt.auxio.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
@ -51,14 +51,11 @@ abstract class ForegroundServiceNotification(context: Context, info: ChannelInfo
|
||||||
*/
|
*/
|
||||||
abstract val code: Int
|
abstract val code: Int
|
||||||
|
|
||||||
/**
|
/** Post this notification using [NotificationManagerCompat]. */
|
||||||
* Post this notification using [NotificationManagerCompat].
|
|
||||||
*/
|
|
||||||
fun post() {
|
fun post() {
|
||||||
// This is safe to call without the POST_NOTIFICATIONS permission, as it's a foreground
|
// This is safe to call without the POST_NOTIFICATIONS permission, as it's a foreground
|
||||||
// notification.
|
// notification.
|
||||||
@Suppress("MissingPermission")
|
@Suppress("MissingPermission") notificationManager.notify(code, build())
|
||||||
notificationManager.notify(code, build())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -33,7 +33,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentAboutBinding
|
import org.oxycblt.auxio.databinding.FragmentAboutBinding
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
|
|
@ -35,13 +35,13 @@ import org.oxycblt.auxio.music.storage.MusicDirectories
|
||||||
import org.oxycblt.auxio.playback.ActionMode
|
import org.oxycblt.auxio.playback.ActionMode
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
|
||||||
import org.oxycblt.auxio.settings.accent.Accent
|
import org.oxycblt.auxio.ui.accent.Accent
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings.
|
* A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. Object
|
||||||
* Object mutability
|
* mutability
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class Settings(private val context: Context, private val callback: Callback? = null) :
|
class Settings(private val context: Context, private val callback: Callback? = null) :
|
||||||
|
@ -55,8 +55,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate any settings from an old version into their modern counterparts. This can cause
|
* Migrate any settings from an old version into their modern counterparts. This can cause data
|
||||||
* data loss depending on the feasibility of a migration.
|
* loss depending on the feasibility of a migration.
|
||||||
*/
|
*/
|
||||||
fun migrate() {
|
fun migrate() {
|
||||||
if (inner.contains(OldKeys.KEY_ACCENT3)) {
|
if (inner.contains(OldKeys.KEY_ACCENT3)) {
|
||||||
|
@ -153,8 +153,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release this instance and any callbacks held by it. This is not needed if no [Callback]
|
* Release this instance and any callbacks held by it. This is not needed if no [Callback] was
|
||||||
* was originally attached.
|
* originally attached.
|
||||||
*/
|
*/
|
||||||
fun release() {
|
fun release() {
|
||||||
inner.unregisterOnSharedPreferenceChangeListener(this)
|
inner.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
|
@ -164,9 +164,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
unlikelyToBeNull(callback).onSettingChanged(key)
|
unlikelyToBeNull(callback).onSettingChanged(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** TODO: Remove this */
|
||||||
* TODO: Remove this
|
|
||||||
*/
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onSettingChanged(key: String)
|
fun onSettingChanged(key: String)
|
||||||
}
|
}
|
||||||
|
@ -264,8 +262,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
?: MusicMode.SONGS
|
?: MusicMode.SONGS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What MusicParent item to play from when a Song is played from the detail view.
|
* What MusicParent item to play from when a Song is played from the detail view. Will be null
|
||||||
* Will be null if configured to play from the currently shown item.
|
* if configured to play from the currently shown item.
|
||||||
*/
|
*/
|
||||||
val detailPlaybackMode: MusicMode?
|
val detailPlaybackMode: MusicMode?
|
||||||
get() =
|
get() =
|
||||||
|
@ -329,8 +327,8 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A string of characters representing the desired separator characters to denote
|
* A string of characters representing the desired separator characters to denote multi-value
|
||||||
* multi-value tags.
|
* tags.
|
||||||
*/
|
*/
|
||||||
var musicSeparators: String?
|
var musicSeparators: String?
|
||||||
// Differ from convention and store a string of separator characters instead of an int
|
// Differ from convention and store a string of separator characters instead of an int
|
||||||
|
|
|
@ -23,7 +23,7 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.transition.MaterialFadeThrough
|
import com.google.android.material.transition.MaterialFadeThrough
|
||||||
import org.oxycblt.auxio.databinding.FragmentSettingsBinding
|
import org.oxycblt.auxio.databinding.FragmentSettingsBinding
|
||||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Fragment] wrapper containing the preference fragment and a companion Toolbar.
|
* A [Fragment] wrapper containing the preference fragment and a companion Toolbar.
|
||||||
|
|
|
@ -114,8 +114,8 @@ constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the index of the current value.
|
* Get the index of the current value.
|
||||||
* @return The index of the current value within [values], or -1 if the [IntListPreference]
|
* @return The index of the current value within [values], or -1 if the [IntListPreference] is
|
||||||
* is not set.
|
* not set.
|
||||||
*/
|
*/
|
||||||
fun getValueIndex(): Int {
|
fun getValueIndex(): Int {
|
||||||
val curValue = currentValue
|
val curValue = currentValue
|
||||||
|
@ -148,9 +148,7 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Copy of ListPreference's [Preference.SummaryProvider] for this [IntListPreference]. */
|
||||||
* Copy of ListPreference's [Preference.SummaryProvider] for this [IntListPreference].
|
|
||||||
*/
|
|
||||||
private inner class IntListSummaryProvider : SummaryProvider<IntListPreference> {
|
private inner class IntListSummaryProvider : SummaryProvider<IntListPreference> {
|
||||||
override fun provideSummary(preference: IntListPreference): CharSequence {
|
override fun provideSummary(preference: IntListPreference): CharSequence {
|
||||||
val index = getValueIndex()
|
val index = getValueIndex()
|
||||||
|
|
|
@ -22,8 +22,8 @@ import android.util.AttributeSet
|
||||||
import androidx.preference.DialogPreference
|
import androidx.preference.DialogPreference
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a [DialogPreference] to be instantiatable. This has no purpose other to ensure that
|
* Wraps a [DialogPreference] to be instantiatable. This has no purpose other to ensure that custom
|
||||||
* custom dialog preferences are handled.
|
* dialog preferences are handled.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class WrappedDialogPreference
|
class WrappedDialogPreference
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.shared
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
@ -35,8 +35,8 @@ import org.oxycblt.auxio.util.coordinatorLayoutBehavior
|
||||||
* 1. Lift state failing to update when list data changes.
|
* 1. Lift state failing to update when list data changes.
|
||||||
* 2. Expansion causing jumping in [RecyclerView] instances.
|
* 2. Expansion causing jumping in [RecyclerView] instances.
|
||||||
*
|
*
|
||||||
* Note: This layout relies on [AppBarLayout.liftOnScrollTargetViewId] to figure out what
|
* Note: This layout relies on [AppBarLayout.liftOnScrollTargetViewId] to figure out what scrolling
|
||||||
* scrolling view to use. Failure to specify this will result in the layout not working.
|
* view to use. Failure to specify this will result in the layout not working.
|
||||||
*
|
*
|
||||||
* Derived from Material Files: https://github.com/zhanghai/MaterialFiles
|
* Derived from Material Files: https://github.com/zhanghai/MaterialFiles
|
||||||
*
|
*
|
||||||
|
@ -70,8 +70,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
/**
|
/**
|
||||||
* Expand this [AppBarLayout] with respect to the given [RecyclerView], preventing it from
|
* Expand this [AppBarLayout] with respect to the given [RecyclerView], preventing it from
|
||||||
* jumping around.
|
* jumping around.
|
||||||
* @param recycler [RecyclerView] to expand with, or null if one is currently unavailable.
|
* @param recycler [RecyclerView] to expand with, or null if one is currently unavailable. TODO:
|
||||||
* TODO: Is it possible to use liftOnScrollTargetViewId to avoid the [RecyclerView] argument?
|
* Is it possible to use liftOnScrollTargetViewId to avoid the [RecyclerView] argument?
|
||||||
*/
|
*/
|
||||||
fun expandWithRecycler(recycler: RecyclerView?) {
|
fun expandWithRecycler(recycler: RecyclerView?) {
|
||||||
setExpanded(true)
|
setExpanded(true)
|
||||||
|
@ -108,13 +108,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [AppBarLayout.OnOffsetChangedListener] that will automatically move the given
|
* An [AppBarLayout.OnOffsetChangedListener] that will automatically move the given
|
||||||
* [RecyclerView] as the [AppBarLayout] expands. Should be added right when the view
|
* [RecyclerView] as the [AppBarLayout] expands. Should be added right when the view is
|
||||||
* is expanding. Will be removed automatically.
|
* expanding. Will be removed automatically.
|
||||||
* @param recycler [RecyclerView] to scroll with the [AppBarLayout].
|
* @param recycler [RecyclerView] to scroll with the [AppBarLayout].
|
||||||
*/
|
*/
|
||||||
private class ExpansionHackListener(private val recycler: RecyclerView) :
|
private class ExpansionHackListener(private val recycler: RecyclerView) :
|
||||||
OnOffsetChangedListener {
|
OnOffsetChangedListener {
|
||||||
private val offsetAnimationMaxEndTime = (AnimationUtils.currentAnimationTimeMillis() +
|
private val offsetAnimationMaxEndTime =
|
||||||
|
(AnimationUtils.currentAnimationTimeMillis() +
|
||||||
APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION)
|
APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION)
|
||||||
private var currentVerticalOffset: Int? = null
|
private var currentVerticalOffset: Int? = null
|
||||||
|
|
||||||
|
@ -123,8 +124,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
AnimationUtils.currentAnimationTimeMillis() > offsetAnimationMaxEndTime) {
|
AnimationUtils.currentAnimationTimeMillis() > offsetAnimationMaxEndTime) {
|
||||||
// AppBarLayout crashes with IndexOutOfBoundsException when a non-last listener
|
// AppBarLayout crashes with IndexOutOfBoundsException when a non-last listener
|
||||||
// removes itself, so we have to do the removal asynchronously.
|
// removes itself, so we have to do the removal asynchronously.
|
||||||
appBarLayout.postOnAnimation {
|
appBarLayout.postOnAnimation { appBarLayout.removeOnOffsetChangedListener(this) }
|
||||||
appBarLayout.removeOnOffsetChangedListener(this) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If possible, scroll by the offset delta between this update and the last update.
|
// If possible, scroll by the offset delta between this update and the last update.
|
||||||
|
@ -137,9 +137,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/** @see AppBarLayout.BaseBehavior.MAX_OFFSET_ANIMATION_DURATION */
|
||||||
* @see AppBarLayout.BaseBehavior.MAX_OFFSET_ANIMATION_DURATION
|
|
||||||
*/
|
|
||||||
private const val APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION = 600
|
private const val APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION = 600
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.shared
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
|
@ -25,14 +25,11 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/** A [ViewModel] that handles complicated navigation functionality. */
|
||||||
* A [ViewModel] that handles complicated navigation functionality.
|
|
||||||
*/
|
|
||||||
class NavigationViewModel : ViewModel() {
|
class NavigationViewModel : ViewModel() {
|
||||||
private val _mainNavigationAction = MutableStateFlow<MainNavigationAction?>(null)
|
private val _mainNavigationAction = MutableStateFlow<MainNavigationAction?>(null)
|
||||||
/**
|
/**
|
||||||
* Flag for navigation within the main navigation graph. Only intended for use by
|
* Flag for navigation within the main navigation graph. Only intended for use by MainFragment.
|
||||||
* MainFragment.
|
|
||||||
*/
|
*/
|
||||||
val mainNavigationAction: StateFlow<MainNavigationAction?>
|
val mainNavigationAction: StateFlow<MainNavigationAction?>
|
||||||
get() = _mainNavigationAction
|
get() = _mainNavigationAction
|
||||||
|
@ -47,17 +44,17 @@ class NavigationViewModel : ViewModel() {
|
||||||
|
|
||||||
private val _exploreNavigationArtists = MutableStateFlow<List<Artist>?>(null)
|
private val _exploreNavigationArtists = MutableStateFlow<List<Artist>?>(null)
|
||||||
/**
|
/**
|
||||||
* Variation of [exploreNavigationItem] for situations where the choice of [Artist]
|
* Variation of [exploreNavigationItem] for situations where the choice of [Artist] to navigate
|
||||||
* to navigate to is ambiguous. Only intended for use by MainFragment, as the resolved
|
* to is ambiguous. Only intended for use by MainFragment, as the resolved choice will
|
||||||
* choice will eventually be assigned to [exploreNavigationItem].
|
* eventually be assigned to [exploreNavigationItem].
|
||||||
*/
|
*/
|
||||||
val exploreNavigationArtists: StateFlow<List<Artist>?>
|
val exploreNavigationArtists: StateFlow<List<Artist>?>
|
||||||
get() = _exploreNavigationArtists
|
get() = _exploreNavigationArtists
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to something in the main navigation graph. This can be used by UIs in the explore
|
* Navigate to something in the main navigation graph. This can be used by UIs in the explore
|
||||||
* navigation graph to trigger navigation in the higher-level main navigation graph.
|
* navigation graph to trigger navigation in the higher-level main navigation graph. Will do
|
||||||
* Will do nothing if already navigating.
|
* nothing if already navigating.
|
||||||
* @param action The [MainNavigationAction] to perform.
|
* @param action The [MainNavigationAction] to perform.
|
||||||
*/
|
*/
|
||||||
fun mainNavigateTo(action: MainNavigationAction) {
|
fun mainNavigateTo(action: MainNavigationAction) {
|
||||||
|
@ -81,8 +78,7 @@ class NavigationViewModel : ViewModel() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to a given [Music] item. Will do nothing if already navigating.
|
* Navigate to a given [Music] item. Will do nothing if already navigating.
|
||||||
* @param item The [Music] to navigate to.
|
* @param item The [Music] to navigate to. TODO: Extend to song properties???
|
||||||
* TODO: Extend to song properties???
|
|
||||||
*/
|
*/
|
||||||
fun exploreNavigateTo(item: Music) {
|
fun exploreNavigateTo(item: Music) {
|
||||||
if (_exploreNavigationItem.value != null) {
|
if (_exploreNavigationItem.value != null) {
|
||||||
|
@ -96,8 +92,8 @@ class NavigationViewModel : ViewModel() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to an [Artist] out of a list of [Artist]s, like [exploreNavigateTo].
|
* Navigate to an [Artist] out of a list of [Artist]s, like [exploreNavigateTo].
|
||||||
* @param artists The [Artist]s to navigate to. In the case of multiple artists, the
|
* @param artists The [Artist]s to navigate to. In the case of multiple artists, the user will
|
||||||
* user will be prompted with a choice on which [Artist] to navigate to.
|
* be prompted with a choice on which [Artist] to navigate to.
|
||||||
*/
|
*/
|
||||||
fun exploreNavigateTo(artists: List<Artist>) {
|
fun exploreNavigateTo(artists: List<Artist>) {
|
||||||
if (_exploreNavigationArtists.value != null) {
|
if (_exploreNavigationArtists.value != null) {
|
||||||
|
@ -126,8 +122,8 @@ class NavigationViewModel : ViewModel() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the possible actions within the main navigation graph. This can be used with
|
* Represents the possible actions within the main navigation graph. This can be used with
|
||||||
* [NavigationViewModel] to initiate navigation in the main navigation graph from anywhere
|
* [NavigationViewModel] to initiate navigation in the main navigation graph from anywhere in the
|
||||||
* in the app, including outside the main navigation graph.
|
* app, including outside the main navigation graph.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
sealed class MainNavigationAction {
|
sealed class MainNavigationAction {
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.shared
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -86,8 +86,8 @@ abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delegate to automatically create and destroy an object derived from the [ViewBinding].
|
* Delegate to automatically create and destroy an object derived from the [ViewBinding]. TODO:
|
||||||
* TODO: Phase this out, it's really dumb
|
* Phase this out, it's really dumb
|
||||||
* @param create Block to create the object from the [ViewBinding].
|
* @param create Block to create the object from the [ViewBinding].
|
||||||
*/
|
*/
|
||||||
fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> {
|
fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> {
|
||||||
|
@ -140,9 +140,7 @@ abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() {
|
||||||
logD("Fragment destroyed")
|
logD("Fragment destroyed")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Internal implementation of [lifecycleObject]. */
|
||||||
* Internal implementation of [lifecycleObject].
|
|
||||||
*/
|
|
||||||
private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) {
|
private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) {
|
||||||
fun populate(binding: VB) {
|
fun populate(binding: VB) {
|
||||||
data = create(binding)
|
data = create(binding)
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.shared
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -76,8 +76,8 @@ abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delegate to automatically create and destroy an object derived from the [ViewBinding].
|
* Delegate to automatically create and destroy an object derived from the [ViewBinding]. TODO:
|
||||||
* TODO: Phase this out, it's really dumb
|
* Phase this out, it's really dumb
|
||||||
* @param create Block to create the object from the [ViewBinding].
|
* @param create Block to create the object from the [ViewBinding].
|
||||||
*/
|
*/
|
||||||
fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> {
|
fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> {
|
||||||
|
@ -121,9 +121,7 @@ abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
|
||||||
logD("Fragment destroyed")
|
logD("Fragment destroyed")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Internal implementation of [lifecycleObject]. */
|
||||||
* Internal implementation of [lifecycleObject].
|
|
||||||
*/
|
|
||||||
private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) {
|
private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) {
|
||||||
fun populate(binding: VB) {
|
fun populate(binding: VB) {
|
||||||
data = create(binding)
|
data = create(binding)
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings.accent
|
package org.oxycblt.auxio.ui.accent
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -120,8 +120,7 @@ class Accent private constructor(val index: Int) : Item {
|
||||||
val theme: Int
|
val theme: Int
|
||||||
get() = ACCENT_THEMES[index]
|
get() = ACCENT_THEMES[index]
|
||||||
/**
|
/**
|
||||||
* The black theme resource for this accent. Identical to [theme], but with a black
|
* The black theme resource for this accent. Identical to [theme], but with a black background.
|
||||||
* background.
|
|
||||||
*/
|
*/
|
||||||
val blackTheme: Int
|
val blackTheme: Int
|
||||||
get() = ACCENT_BLACK_THEMES[index]
|
get() = ACCENT_BLACK_THEMES[index]
|
||||||
|
@ -137,8 +136,8 @@ class Accent private constructor(val index: Int) : Item {
|
||||||
/**
|
/**
|
||||||
* Create a new instance.
|
* Create a new instance.
|
||||||
* @param index The unique number for this particular accent.
|
* @param index The unique number for this particular accent.
|
||||||
* @return A new [Accent] with the specified [index]. If [index] is not within the
|
* @return A new [Accent] with the specified [index]. If [index] is not within the range of
|
||||||
* range of valid accents, [index] will be [DEFAULT] instead.
|
* valid accents, [index] will be [DEFAULT] instead.
|
||||||
*/
|
*/
|
||||||
fun from(index: Int): Accent {
|
fun from(index: Int): Accent {
|
||||||
if (index !in 0 until MAX) {
|
if (index !in 0 until MAX) {
|
||||||
|
@ -148,9 +147,7 @@ class Accent private constructor(val index: Int) : Item {
|
||||||
return Accent(index)
|
return Accent(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The default accent. */
|
||||||
* The default accent.
|
|
||||||
*/
|
|
||||||
val DEFAULT =
|
val DEFAULT =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
// Use dynamic coloring on devices that support it.
|
// Use dynamic coloring on devices that support it.
|
||||||
|
@ -160,9 +157,7 @@ class Accent private constructor(val index: Int) : Item {
|
||||||
5
|
5
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The amount of valid accents. */
|
||||||
* The amount of valid accents.
|
|
||||||
*/
|
|
||||||
val MAX =
|
val MAX =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
ACCENT_THEMES.size
|
ACCENT_THEMES.size
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings.accent
|
package org.oxycblt.auxio.ui.accent
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings.accent
|
package org.oxycblt.auxio.ui.accent
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -26,7 +26,7 @@ import org.oxycblt.auxio.databinding.DialogAccentBinding
|
||||||
import org.oxycblt.auxio.list.ClickableListListener
|
import org.oxycblt.auxio.list.ClickableListListener
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
@ -35,7 +35,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* A [ViewBindingDialogFragment] that allows the user to configure the current [Accent].
|
* A [ViewBindingDialogFragment] that allows the user to configure the current [Accent].
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>(), ClickableListListener {
|
class AccentCustomizeDialog :
|
||||||
|
ViewBindingDialogFragment<DialogAccentBinding>(), ClickableListListener {
|
||||||
private var accentAdapter = AccentAdapter(this)
|
private var accentAdapter = AccentAdapter(this)
|
||||||
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
|
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings.accent
|
package org.oxycblt.auxio.ui.accent
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
@ -27,8 +27,8 @@ import org.oxycblt.auxio.util.getDimenPixels
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [GridLayoutManager] that automatically sets the span size in order to use the most possible
|
* A [GridLayoutManager] that automatically sets the span size in order to use the most possible
|
||||||
* space in the [RecyclerView].
|
* space in the [RecyclerView]. Derived from this StackOverflow answer:
|
||||||
* Derived from this StackOverflow answer: https://stackoverflow.com/a/30256880/14143986
|
* https://stackoverflow.com/a/30256880/14143986
|
||||||
*/
|
*/
|
||||||
class AccentGridLayoutManager(
|
class AccentGridLayoutManager(
|
||||||
context: Context,
|
context: Context,
|
|
@ -47,17 +47,13 @@ import org.oxycblt.auxio.MainActivity
|
||||||
val Context.inflater: LayoutInflater
|
val Context.inflater: LayoutInflater
|
||||||
get() = LayoutInflater.from(this)
|
get() = LayoutInflater.from(this)
|
||||||
|
|
||||||
/**
|
/** Whether the device is in night mode or not. */
|
||||||
* Whether the device is in night mode or not.
|
|
||||||
*/
|
|
||||||
val Context.isNight
|
val Context.isNight
|
||||||
get() =
|
get() =
|
||||||
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
|
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
|
||||||
Configuration.UI_MODE_NIGHT_YES
|
Configuration.UI_MODE_NIGHT_YES
|
||||||
|
|
||||||
/**
|
/** Whether the device is in landscape mode or not. */
|
||||||
* Whether the device is in landscape mode or not.
|
|
||||||
*/
|
|
||||||
val Context.isLandscape
|
val Context.isLandscape
|
||||||
get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
|
||||||
|
@ -152,9 +148,7 @@ fun Context.showToast(@StringRes stringRes: Int) {
|
||||||
Toast.makeText(applicationContext, getString(stringRes), Toast.LENGTH_SHORT).show()
|
Toast.makeText(applicationContext, getString(stringRes), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Create a [PendingIntent] that will launch the app activity when launched. */
|
||||||
* Create a [PendingIntent] that will launch the app activity when launched.
|
|
||||||
*/
|
|
||||||
fun Context.newMainPendingIntent(): PendingIntent =
|
fun Context.newMainPendingIntent(): PendingIntent =
|
||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
|
|
|
@ -50,11 +50,10 @@ import kotlinx.coroutines.launch
|
||||||
* Get if this [View] contains the given [PointF], with optional leeway.
|
* Get if this [View] contains the given [PointF], with optional leeway.
|
||||||
* @param x The x value of the point to check.
|
* @param x The x value of the point to check.
|
||||||
* @param y The y value of the point to check.
|
* @param y The y value of the point to check.
|
||||||
* @param minTouchTargetSize A minimum size to use when checking the value.
|
* @param minTouchTargetSize A minimum size to use when checking the value. This can be used to
|
||||||
* This can be used to extend the range where a point is considered "contained"
|
* extend the range where a point is considered "contained" by the [View] beyond it's actual size.
|
||||||
* by the [View] beyond it's actual size.
|
* @return true if the [PointF] is contained by the view, false otherwise. Adapted from
|
||||||
* @return true if the [PointF] is contained by the view, false otherwise.
|
* AndroidFastScroll: https://github.com/zhanghai/AndroidFastScroll
|
||||||
* Adapted from AndroidFastScroll: https://github.com/zhanghai/AndroidFastScroll
|
|
||||||
*/
|
*/
|
||||||
fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) =
|
fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) =
|
||||||
isUnderImpl(x, left, right, (parent as View).width, minTouchTargetSize) &&
|
isUnderImpl(x, left, right, (parent as View).width, minTouchTargetSize) &&
|
||||||
|
@ -66,8 +65,7 @@ fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) =
|
||||||
* @param viewStart The start of the view bounds, on the same axis as [position].
|
* @param viewStart The start of the view bounds, on the same axis as [position].
|
||||||
* @param viewEnd The end of the view bounds, on the same axis as [position]
|
* @param viewEnd The end of the view bounds, on the same axis as [position]
|
||||||
* @param parentEnd The end of the parent bounds, on the same axis as [position].
|
* @param parentEnd The end of the parent bounds, on the same axis as [position].
|
||||||
* @param minTouchTargetSize The minimum size to use when checking if the value is
|
* @param minTouchTargetSize The minimum size to use when checking if the value is in range.
|
||||||
* in range.
|
|
||||||
*/
|
*/
|
||||||
private fun isUnderImpl(
|
private fun isUnderImpl(
|
||||||
position: Float,
|
position: Float,
|
||||||
|
@ -98,27 +96,21 @@ private fun isUnderImpl(
|
||||||
return position >= touchTargetStart && position < touchTargetEnd
|
return position >= touchTargetStart && position < touchTargetEnd
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Whether this [View] is using an RTL layout direction. */
|
||||||
* Whether this [View] is using an RTL layout direction.
|
|
||||||
*/
|
|
||||||
val View.isRtl: Boolean
|
val View.isRtl: Boolean
|
||||||
get() = layoutDirection == View.LAYOUT_DIRECTION_RTL
|
get() = layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||||
|
|
||||||
/**
|
/** Whether this [Drawable] is using an RTL layout direction. */
|
||||||
* Whether this [Drawable] is using an RTL layout direction.
|
|
||||||
*/
|
|
||||||
val Drawable.isRtl: Boolean
|
val Drawable.isRtl: Boolean
|
||||||
get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL
|
get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL
|
||||||
|
|
||||||
/**
|
/** Get a [Context] from a [ViewBinding]'s root [View]. */
|
||||||
* Get a [Context] from a [ViewBinding]'s root [View].
|
|
||||||
*/
|
|
||||||
val ViewBinding.context: Context
|
val ViewBinding.context: Context
|
||||||
get() = root.context
|
get() = root.context
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute if this [RecyclerView] can scroll through their items, or if the items can all fit on
|
* Compute if this [RecyclerView] can scroll through their items, or if the items can all fit on one
|
||||||
* one screen.
|
* screen.
|
||||||
*/
|
*/
|
||||||
fun RecyclerView.canScroll() = computeVerticalScrollRange() > height
|
fun RecyclerView.canScroll() = computeVerticalScrollRange() > height
|
||||||
|
|
||||||
|
@ -131,8 +123,8 @@ val View.coordinatorLayoutBehavior: CoordinatorLayout.Behavior<View>?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect a [StateFlow] into [block] in a lifecycle-aware manner *eventually.* Due to co-routine
|
* Collect a [StateFlow] into [block] in a lifecycle-aware manner *eventually.* Due to co-routine
|
||||||
* launching, the initializing call will occur ~100ms after draw time. If this is not desirable,
|
* launching, the initializing call will occur ~100ms after draw time. If this is not desirable, use
|
||||||
* use [collectImmediately].
|
* [collectImmediately].
|
||||||
* @param stateFlow The [StateFlow] to collect.
|
* @param stateFlow The [StateFlow] to collect.
|
||||||
* @param block The code to run when the [StateFlow] updates.
|
* @param block The code to run when the [StateFlow] updates.
|
||||||
*/
|
*/
|
||||||
|
@ -142,8 +134,8 @@ fun <T> Fragment.collect(stateFlow: StateFlow<T>, block: (T) -> Unit) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect a [StateFlow] into a [block] in a lifecycle-aware manner *immediately.* This will
|
* Collect a [StateFlow] into a [block] in a lifecycle-aware manner *immediately.* This will
|
||||||
* immediately run an initializing call to ensure the UI is set up before draw-time. Note
|
* immediately run an initializing call to ensure the UI is set up before draw-time. Note that this
|
||||||
* that this will result in two initializing calls.
|
* will result in two initializing calls.
|
||||||
* @param stateFlow The [StateFlow] to collect.
|
* @param stateFlow The [StateFlow] to collect.
|
||||||
* @param block The code to run when the [StateFlow] updates.
|
* @param block The code to run when the [StateFlow] updates.
|
||||||
*/
|
*/
|
||||||
|
@ -153,8 +145,8 @@ fun <T> Fragment.collectImmediately(stateFlow: StateFlow<T>, block: (T) -> Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like [collectImmediately], but with two [StateFlow] instances that are collected
|
* Like [collectImmediately], but with two [StateFlow] instances that are collected with the same
|
||||||
* with the same block.
|
* block.
|
||||||
* @param a The first [StateFlow] to collect.
|
* @param a The first [StateFlow] to collect.
|
||||||
* @param b The second [StateFlow] to collect.
|
* @param b The second [StateFlow] to collect.
|
||||||
* @param block The code to run when either [StateFlow] updates.
|
* @param block The code to run when either [StateFlow] updates.
|
||||||
|
@ -173,8 +165,8 @@ fun <T1, T2> Fragment.collectImmediately(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like [collectImmediately], but with three [StateFlow] instances that are collected
|
* Like [collectImmediately], but with three [StateFlow] instances that are collected with the same
|
||||||
* with the same block.
|
* block.
|
||||||
* @param a The first [StateFlow] to collect.
|
* @param a The first [StateFlow] to collect.
|
||||||
* @param b The second [StateFlow] to collect.
|
* @param b The second [StateFlow] to collect.
|
||||||
* @param c The third [StateFlow] to collect.
|
* @param c The third [StateFlow] to collect.
|
||||||
|
@ -192,9 +184,9 @@ fun <T1, T2, T3> Fragment.collectImmediately(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State].
|
* Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State]. This
|
||||||
* This should always been used when launching [Fragment] co-routines was it will not result
|
* should always been used when launching [Fragment] co-routines was it will not result in
|
||||||
* in unexpected behavior.
|
* unexpected behavior.
|
||||||
* @param state The [Lifecycle.State] to launch the co-routine in.
|
* @param state The [Lifecycle.State] to launch the co-routine in.
|
||||||
* @param block The block to run in the co-routine.
|
* @param block The block to run in the co-routine.
|
||||||
* @see repeatOnLifecycle
|
* @see repeatOnLifecycle
|
||||||
|
@ -208,40 +200,36 @@ private fun Fragment.launch(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An extension to [viewModels] that automatically provides an
|
* An extension to [viewModels] that automatically provides an
|
||||||
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel]
|
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] is used.
|
||||||
* is used.
|
|
||||||
*/
|
*/
|
||||||
inline fun <reified T : AndroidViewModel> Fragment.androidViewModels() =
|
inline fun <reified T : AndroidViewModel> Fragment.androidViewModels() =
|
||||||
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) }
|
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An extension to [viewModels] that automatically provides an
|
* An extension to [viewModels] that automatically provides an
|
||||||
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel]
|
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] is used. Note
|
||||||
* is used. Note that this implementation is for an [AppCompatActivity], and thus
|
* that this implementation is for an [AppCompatActivity], and thus makes this functionally
|
||||||
* makes this functionally equivalent in scope to [androidActivityViewModels].
|
* equivalent in scope to [androidActivityViewModels].
|
||||||
*/
|
*/
|
||||||
inline fun <reified T : AndroidViewModel> AppCompatActivity.androidViewModels() =
|
inline fun <reified T : AndroidViewModel> AppCompatActivity.androidViewModels() =
|
||||||
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(application) }
|
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(application) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An extension to [activityViewModels] that automatically provides an
|
* An extension to [activityViewModels] that automatically provides an
|
||||||
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel]
|
* [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] is used.
|
||||||
* is used.
|
|
||||||
*/
|
*/
|
||||||
inline fun <reified T : AndroidViewModel> Fragment.androidActivityViewModels() =
|
inline fun <reified T : AndroidViewModel> Fragment.androidActivityViewModels() =
|
||||||
activityViewModels<T> {
|
activityViewModels<T> {
|
||||||
ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
|
ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The [Context] provided to an [AndroidViewModel]. */
|
||||||
* The [Context] provided to an [AndroidViewModel].
|
|
||||||
*/
|
|
||||||
inline val AndroidViewModel.context: Context
|
inline val AndroidViewModel.context: Context
|
||||||
get() = getApplication()
|
get() = getApplication()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor]
|
* Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor] is
|
||||||
* is loaded. The block will be called with [use], allowing for automatic cleanup of [Cursor]
|
* loaded. The block will be called with [use], allowing for automatic cleanup of [Cursor]
|
||||||
* resources.
|
* resources.
|
||||||
* @param tableName The name of the table to query all columns in.
|
* @param tableName The name of the table to query all columns in.
|
||||||
* @param block The code block to run with the loaded [Cursor].
|
* @param block The code block to run with the loaded [Cursor].
|
||||||
|
@ -250,8 +238,8 @@ inline fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R)
|
||||||
query(tableName, null, null, null, null, null, null)?.use(block)
|
query(tableName, null, null, null, null, null, null)?.use(block)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner
|
* Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner This
|
||||||
* This can be used to prevent [View] elements from intersecting with the navigation bars.
|
* can be used to prevent [View] elements from intersecting with the navigation bars.
|
||||||
*/
|
*/
|
||||||
val WindowInsets.systemBarInsetsCompat: Insets
|
val WindowInsets.systemBarInsetsCompat: Insets
|
||||||
get() =
|
get() =
|
||||||
|
@ -266,9 +254,9 @@ val WindowInsets.systemBarInsetsCompat: Insets
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the "System Gesture" [Insets] in this [WindowInsets] instance in a version-compatible manner
|
* Get the "System Gesture" [Insets] in this [WindowInsets] instance in a version-compatible manner
|
||||||
* This can be used to prevent [View] elements from intersecting with the navigation bars and
|
* This can be used to prevent [View] elements from intersecting with the navigation bars and their
|
||||||
* their extended gesture hit-boxes. Note that "System Bar" insets will be used if the system
|
* extended gesture hit-boxes. Note that "System Bar" insets will be used if the system does not
|
||||||
* does not provide gesture insets.
|
* provide gesture insets.
|
||||||
*/
|
*/
|
||||||
val WindowInsets.systemGestureInsetsCompat: Insets
|
val WindowInsets.systemGestureInsetsCompat: Insets
|
||||||
get() =
|
get() =
|
||||||
|
|
|
@ -53,8 +53,8 @@ fun Long.nonZeroOrNull() = if (this > 0) this else null
|
||||||
fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null
|
fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazily set up a reflected field. Automatically handles visibility changes.
|
* Lazily set up a reflected field. Automatically handles visibility changes. Adapted from Material
|
||||||
* Adapted from Material Files: https://github.com/zhanghai/MaterialFiles
|
* Files: https://github.com/zhanghai/MaterialFiles
|
||||||
* @param clazz The [KClass] to reflect into.
|
* @param clazz The [KClass] to reflect into.
|
||||||
* @param field The name of the field to obtain.
|
* @param field The name of the field to obtain.
|
||||||
*/
|
*/
|
||||||
|
@ -62,8 +62,8 @@ fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy {
|
||||||
clazz.java.getDeclaredField(field).also { it.isAccessible = true }
|
clazz.java.getDeclaredField(field).also { it.isAccessible = true }
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Lazily set up a reflected method. Automatically handles visibility changes.
|
* Lazily set up a reflected method. Automatically handles visibility changes. Adapted from Material
|
||||||
* Adapted from Material Files: https://github.com/zhanghai/MaterialFiles
|
* Files: https://github.com/zhanghai/MaterialFiles
|
||||||
* @param clazz The [KClass] to reflect into.
|
* @param clazz The [KClass] to reflect into.
|
||||||
* @param field The name of the method to obtain.
|
* @param field The name of the method to obtain.
|
||||||
*/
|
*/
|
||||||
|
@ -72,9 +72,9 @@ fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert that the execution is currently on a background thread. This is helpful for
|
* Assert that the execution is currently on a background thread. This is helpful for functions that
|
||||||
* functions that don't necessarily require suspend, but still want to ensure that they
|
* don't necessarily require suspend, but still want to ensure that they are being called with a
|
||||||
* are being called with a co-routine.
|
* co-routine.
|
||||||
* @throws IllegalStateException If the execution is not on a background thread.
|
* @throws IllegalStateException If the execution is not on a background thread.
|
||||||
*/
|
*/
|
||||||
fun requireBackgroundThread() {
|
fun requireBackgroundThread() {
|
||||||
|
|
|
@ -52,15 +52,15 @@ fun Any.logW(msg: String) = Log.w(autoTag, msg)
|
||||||
fun Any.logE(msg: String) = Log.e(autoTag, msg)
|
fun Any.logE(msg: String) = Log.e(autoTag, msg)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The LogCat-suitable tag for this string. Consists of the object's name, or "Anonymous Object"
|
* The LogCat-suitable tag for this string. Consists of the object's name, or "Anonymous Object" if
|
||||||
* if the object does not exist.
|
* the object does not exist.
|
||||||
*/
|
*/
|
||||||
private val Any.autoTag: String
|
private val Any.autoTag: String
|
||||||
get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}"
|
get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Please don't plagiarize Auxio!
|
* Please don't plagiarize Auxio! You are free to remove this as long as you continue to keep your
|
||||||
* You are free to remove this as long as you continue to keep your source open.
|
* source open.
|
||||||
*/
|
*/
|
||||||
@Suppress("KotlinConstantConditions")
|
@Suppress("KotlinConstantConditions")
|
||||||
private fun copyleftNotice(): Boolean {
|
private fun copyleftNotice(): Boolean {
|
||||||
|
|
|
@ -35,9 +35,8 @@ import org.oxycblt.auxio.util.getDimenPixels
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component that manages the "Now Playing" state.
|
* A component that manages the "Now Playing" state. This is kept separate from the [WidgetProvider]
|
||||||
* This is kept separate from the [WidgetProvider] itself to prevent possible memory
|
* itself to prevent possible memory leaks and enable extension to more widgets in the future.
|
||||||
* leaks and enable extension to more widgets in the future.
|
|
||||||
* @param context [Context] required to manage AppWidgetProviders.
|
* @param context [Context] required to manage AppWidgetProviders.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
@ -52,9 +51,7 @@ class WidgetComponent(private val context: Context) :
|
||||||
playbackManager.addCallback(this)
|
playbackManager.addCallback(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Update [WidgetProvider] with the current playback state. */
|
||||||
* Update [WidgetProvider] with the current playback state.
|
|
||||||
*/
|
|
||||||
fun update() {
|
fun update() {
|
||||||
val song = playbackManager.song
|
val song = playbackManager.song
|
||||||
if (song == null) {
|
if (song == null) {
|
||||||
|
@ -104,9 +101,7 @@ class WidgetComponent(private val context: Context) :
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Release this instance, preventing any further events from updating the widget instances. */
|
||||||
* Release this instance, preventing any further events from updating the widget instances.
|
|
||||||
*/
|
|
||||||
fun release() {
|
fun release() {
|
||||||
provider.release()
|
provider.release()
|
||||||
settings.release()
|
settings.release()
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue