all: remove superfluous comments
Remove superflous comments that really add nothing.
This commit is contained in:
parent
7415c28e2d
commit
b38b8a909f
57 changed files with 481 additions and 960 deletions
|
@ -27,7 +27,7 @@ import coil.ImageLoaderFactory
|
|||
import coil.request.CachePolicy
|
||||
import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher
|
||||
import org.oxycblt.auxio.image.extractor.ArtistImageFetcher
|
||||
import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFractory
|
||||
import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFactory
|
||||
import org.oxycblt.auxio.image.extractor.GenreImageFetcher
|
||||
import org.oxycblt.auxio.image.extractor.MusicKeyer
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
|
@ -68,7 +68,7 @@ class AuxioApp : Application(), ImageLoaderFactory {
|
|||
add(GenreImageFetcher.Factory())
|
||||
}
|
||||
// Use our own crossfade with error drawable support
|
||||
.transitionFactory(ErrorCrossfadeTransitionFractory())
|
||||
.transitionFactory(ErrorCrossfadeTransitionFactory())
|
||||
// Not downloading anything, so no disk-caching
|
||||
.diskCachePolicy(CachePolicy.DISABLED)
|
||||
.build()
|
||||
|
|
|
@ -17,144 +17,102 @@
|
|||
|
||||
package org.oxycblt.auxio
|
||||
|
||||
/** A table containing all unique integer codes that Auxio uses. */
|
||||
/**
|
||||
* A table containing all of the magic integer codes that the codebase has currently reserved.
|
||||
* May be non-contiguous.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
object IntegerTable {
|
||||
/** SongViewHolder */
|
||||
const val VIEW_TYPE_SONG = 0xA000
|
||||
|
||||
/** AlbumViewHolder */
|
||||
const val VIEW_TYPE_ALBUM = 0xA001
|
||||
|
||||
/** ArtistViewHolder */
|
||||
const val VIEW_TYPE_ARTIST = 0xA002
|
||||
|
||||
/** GenreViewHolder */
|
||||
const val VIEW_TYPE_GENRE = 0xA003
|
||||
|
||||
/** HeaderViewHolder */
|
||||
const val VIEW_TYPE_HEADER = 0xA004
|
||||
|
||||
/** SortHeaderViewHolder */
|
||||
const val VIEW_TYPE_SORT_HEADER = 0xA005
|
||||
|
||||
/** AlbumDetailViewHolder */
|
||||
const val VIEW_TYPE_ALBUM_DETAIL = 0xA006
|
||||
|
||||
/** AlbumSongViewHolder */
|
||||
const val VIEW_TYPE_ALBUM_SONG = 0xA007
|
||||
|
||||
/** ArtistDetailViewHolder */
|
||||
const val VIEW_TYPE_ARTIST_DETAIL = 0xA008
|
||||
|
||||
/** ArtistAlbumViewHolder */
|
||||
const val VIEW_TYPE_ARTIST_ALBUM = 0xA009
|
||||
|
||||
/** ArtistSongViewHolder */
|
||||
const val VIEW_TYPE_ARTIST_SONG = 0xA00A
|
||||
|
||||
/** GenreDetailViewHolder */
|
||||
const val VIEW_TYPE_GENRE_DETAIL = 0xA00B
|
||||
|
||||
/** DiscHeaderViewHolder */
|
||||
const val VIEW_TYPE_DISC_HEADER = 0xA00C
|
||||
|
||||
/** "Music playback" notification code */
|
||||
const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0
|
||||
|
||||
/** "Music loading" notification code */
|
||||
const val INDEXER_NOTIFICATION_CODE = 0xA0A1
|
||||
|
||||
/** Intent request code */
|
||||
/** MainActivity Intent request code */
|
||||
const val REQUEST_CODE = 0xA0C0
|
||||
|
||||
/** RepeatMode.NONE */
|
||||
const val REPEAT_MODE_NONE = 0xA100
|
||||
|
||||
/** RepeatMode.ALL */
|
||||
const val REPEAT_MODE_ALL = 0xA101
|
||||
|
||||
/** RepeatMode.TRACK */
|
||||
const val REPEAT_MODE_TRACK = 0xA102
|
||||
|
||||
/** PlaybackMode.IN_ARTIST */
|
||||
const val PLAYBACK_MODE_IN_ARTIST = 0xA104
|
||||
|
||||
/** PlaybackMode.IN_ALBUM */
|
||||
const val PLAYBACK_MODE_IN_ALBUM = 0xA105
|
||||
|
||||
/** PlaybackMode.ALL_SONGS */
|
||||
const val PLAYBACK_MODE_ALL_SONGS = 0xA106
|
||||
|
||||
/** DisplayMode.NONE (No Longer used but still reserved) */
|
||||
// const val DISPLAY_MODE_NONE = 0xA107
|
||||
/** MusicMode._GENRES */
|
||||
const val MUSIC_MODE_GENRES = 0xA108
|
||||
|
||||
/** MusicMode._ARTISTS */
|
||||
const val MUSIC_MODE_ARTISTS = 0xA109
|
||||
|
||||
/** MusicMode._ALBUMS */
|
||||
const val MUSIC_MODE_ALBUMS = 0xA10A
|
||||
|
||||
/** MusicMode._SONGS */
|
||||
/** MusicMode.SONGS */
|
||||
const val MUSIC_MODE_SONGS = 0xA10B
|
||||
|
||||
// Note: Sort integer codes are non-contiguous due to significant amounts of time
|
||||
// passing between the additions of new sort modes.
|
||||
|
||||
/** Sort.ByName */
|
||||
const val SORT_BY_NAME = 0xA10C
|
||||
|
||||
/** Sort.ByArtist */
|
||||
const val SORT_BY_ARTIST = 0xA10D
|
||||
|
||||
/** Sort.ByAlbum */
|
||||
const val SORT_BY_ALBUM = 0xA10E
|
||||
|
||||
/** Sort.ByYear */
|
||||
const val SORT_BY_YEAR = 0xA10F
|
||||
|
||||
/** Sort.ByDuration */
|
||||
const val SORT_BY_DURATION = 0xA114
|
||||
|
||||
/** Sort.ByCount */
|
||||
const val SORT_BY_COUNT = 0xA115
|
||||
|
||||
/** Sort.ByDisc */
|
||||
const val SORT_BY_DISC = 0xA116
|
||||
|
||||
/** Sort.ByTrack */
|
||||
const val SORT_BY_TRACK = 0xA117
|
||||
|
||||
/** Sort.ByDateAdded */
|
||||
const val SORT_BY_DATE_ADDED = 0xA118
|
||||
|
||||
/** ReplayGainMode.Off (No longer used but still reserved) */
|
||||
// const val REPLAY_GAIN_MODE_OFF = 0xA110
|
||||
/** ReplayGainMode.Track */
|
||||
const val REPLAY_GAIN_MODE_TRACK = 0xA111
|
||||
|
||||
/** ReplayGainMode.Album */
|
||||
const val REPLAY_GAIN_MODE_ALBUM = 0xA112
|
||||
|
||||
/** ReplayGainMode.Dynamic */
|
||||
const val REPLAY_GAIN_MODE_DYNAMIC = 0xA113
|
||||
|
||||
/** ActionMode.Next */
|
||||
const val ACTION_MODE_NEXT = 0xA119
|
||||
|
||||
/** ActionMode.Repeat */
|
||||
const val ACTION_MODE_REPEAT = 0xA11A
|
||||
|
||||
/** ActionMode.Shuffle */
|
||||
const val ACTION_MODE_SHUFFLE = 0xA11B
|
||||
|
||||
/** CoverMode.Off */
|
||||
const val COVER_MODE_OFF = 0xA11C
|
||||
|
||||
/** CoverMode.MediaStore */
|
||||
const val COVER_MODE_MEDIA_STORE = 0xA11D
|
||||
|
||||
/** CoverMode.Quality */
|
||||
const val COVER_MODE_QUALITY = 0xA11E
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.logD
|
|||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||
|
||||
/**
|
||||
* The single [AppCompatActivity] for Auxio.
|
||||
* Auxio's single [AppCompatActivity].
|
||||
*
|
||||
* TODO: Add error screens
|
||||
*
|
||||
|
@ -85,6 +85,14 @@ class MainActivity : AppCompatActivity() {
|
|||
startIntentAction(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action]
|
||||
* that can be used in the playback system.
|
||||
* @param intent The (new) [Intent] given to this [MainActivity], or null if there
|
||||
* is no intent.
|
||||
* @return true If the analogous [InternalPlayer.Action] to the given [Intent] was started,
|
||||
* false otherwise.
|
||||
*/
|
||||
private fun startIntentAction(intent: Intent?): Boolean {
|
||||
if (intent == null) {
|
||||
return false
|
||||
|
@ -97,7 +105,6 @@ class MainActivity : AppCompatActivity() {
|
|||
// RestoreState action.
|
||||
return true
|
||||
}
|
||||
|
||||
intent.putExtra(KEY_INTENT_USED, true)
|
||||
|
||||
val action =
|
||||
|
@ -106,19 +113,16 @@ class MainActivity : AppCompatActivity() {
|
|||
AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
|
||||
else -> return false
|
||||
}
|
||||
|
||||
playbackModel.startAction(action)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setupTheme() {
|
||||
val settings = Settings(this)
|
||||
|
||||
// Set up the current theme.
|
||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||
|
||||
// The black theme has a completely separate set of styles since style attributes cannot
|
||||
// be modified at runtime.
|
||||
// Set up the color scheme. Note that the black theme has it's own set
|
||||
// of styles since the color schemes cannot be modified at runtime.
|
||||
if (isNight && settings.useBlackTheme) {
|
||||
logD("Applying black theme [accent ${settings.accent}]")
|
||||
setTheme(settings.accent.blackTheme)
|
||||
|
@ -130,7 +134,6 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
private fun setupEdgeToEdge(contentView: View) {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
contentView.setOnApplyWindowInsetsListener { view, insets ->
|
||||
val bars = insets.systemBarInsetsCompat
|
||||
view.updatePadding(left = bars.left, right = bars.right)
|
||||
|
|
|
@ -121,7 +121,7 @@ class MainFragment :
|
|||
collect(navModel.exploreNavigationItem, ::handleExploreNavigation)
|
||||
collect(navModel.exploreNavigationArtists, ::handleExplorePicker)
|
||||
collectImmediately(playbackModel.song, ::updateSong)
|
||||
collect(playbackModel.artistPlaybackPickerSong, ::handlePlaybackPicker)
|
||||
collect(playbackModel.artistPlaybackPickerSong, ::handlePlaybackArtistPicker)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -216,22 +216,25 @@ class MainFragment :
|
|||
if (playbackModel.song.value == null) {
|
||||
// Sometimes lingering drags can un-hide the playback sheet even when we intend to
|
||||
// hide it, make sure we keep it hidden.
|
||||
tryHideAll()
|
||||
tryHideAllSheets()
|
||||
}
|
||||
|
||||
// Since the callback is also reliant on the bottom sheets, we must also update it
|
||||
// every frame.
|
||||
callback.updateEnabledState()
|
||||
callback.invalidateEnabled()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleMainNavigation(action: MainNavigationAction?) {
|
||||
if (action == null) return
|
||||
if (action == null) {
|
||||
// Nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
when (action) {
|
||||
is MainNavigationAction.Expand -> tryExpandAll()
|
||||
is MainNavigationAction.Collapse -> tryCollapseAll()
|
||||
is MainNavigationAction.Expand -> tryExpandSheets()
|
||||
is MainNavigationAction.Collapse -> tryCollapseSheets()
|
||||
// TODO: Figure out how to clear out the selections as one moves between screens.
|
||||
is MainNavigationAction.Directions -> findNavController().navigate(action.directions)
|
||||
}
|
||||
|
@ -241,12 +244,13 @@ class MainFragment :
|
|||
|
||||
private fun handleExploreNavigation(item: Music?) {
|
||||
if (item != null) {
|
||||
tryCollapseAll()
|
||||
tryCollapseSheets()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleExplorePicker(items: List<Artist>?) {
|
||||
if (items != null) {
|
||||
// Navigate to the analogous artist picker dialog.
|
||||
navModel.mainNavigateTo(
|
||||
MainNavigationAction.Directions(
|
||||
MainFragmentDirections.actionPickNavigationArtist(
|
||||
|
@ -257,14 +261,15 @@ class MainFragment :
|
|||
|
||||
private fun updateSong(song: Song?) {
|
||||
if (song != null) {
|
||||
tryUnhideAll()
|
||||
tryShowSheets()
|
||||
} else {
|
||||
tryHideAll()
|
||||
tryHideAllSheets()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePlaybackPicker(song: Song?) {
|
||||
private fun handlePlaybackArtistPicker(song: Song?) {
|
||||
if (song != null) {
|
||||
// Navigate to the analogous artist picker dialog.
|
||||
navModel.mainNavigateTo(
|
||||
MainNavigationAction.Directions(
|
||||
MainFragmentDirections.actionPickPlaybackArtist(song.uid)))
|
||||
|
@ -272,18 +277,18 @@ class MainFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun tryExpandAll() {
|
||||
private fun tryExpandSheets() {
|
||||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
|
||||
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
|
||||
// State is collapsed and non-hidden, expand
|
||||
// Playback sheet is not expanded and not hidden, we can expand it.
|
||||
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryCollapseAll() {
|
||||
private fun tryCollapseSheets() {
|
||||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
|
@ -292,13 +297,12 @@ class MainFragment :
|
|||
// Make sure the queue is also collapsed here.
|
||||
val queueSheetBehavior =
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||
|
||||
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
|
||||
queueSheetBehavior?.state = NeoBottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryUnhideAll() {
|
||||
private fun tryShowSheets() {
|
||||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
|
@ -318,7 +322,7 @@ class MainFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun tryHideAll() {
|
||||
private fun tryHideAllSheets() {
|
||||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
|
@ -342,8 +346,8 @@ class MainFragment :
|
|||
}
|
||||
|
||||
/**
|
||||
* A back press callback that handles how to respond to backwards navigation in the detail
|
||||
* fragments and the playback panel.
|
||||
* A [OnBackPressedCallback] that overrides the back button to first navigate out of
|
||||
* internal app components, such as the Bottom Sheets or Explore Navigation.
|
||||
*/
|
||||
private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
|
@ -353,36 +357,45 @@ class MainFragment :
|
|||
val queueSheetBehavior =
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||
|
||||
// If expanded, collapse the queue sheet first.
|
||||
if (queueSheetBehavior != null &&
|
||||
queueSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
|
||||
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) {
|
||||
// Collapse the queue first if it is expanded.
|
||||
queueSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
|
||||
return
|
||||
}
|
||||
|
||||
// If expanded, collapse the playback sheet next.
|
||||
if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
|
||||
playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) {
|
||||
// Then collapse the playback sheet.
|
||||
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
|
||||
return
|
||||
}
|
||||
|
||||
// Then try to navigate out of the explore navigation fragments (i.e Detail Views)
|
||||
binding.exploreNavHost.findNavController().navigateUp()
|
||||
}
|
||||
|
||||
fun updateEnabledState() {
|
||||
/**
|
||||
* Force this instance to update whether it's enabled or not. If there are no app
|
||||
* components that the back button should close first, the instance is disabled and
|
||||
* back navigation is delegated to the system.
|
||||
*
|
||||
* Normally, this callback would have just called the [MainActivity.onBackPressed]
|
||||
* if there were no components to close, but that prevents adaptive back navigation
|
||||
* from working on Android 14+, so we must do it this way.
|
||||
*/
|
||||
fun invalidateEnabled() {
|
||||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
val queueSheetBehavior =
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||
|
||||
val exploreNavController = binding.exploreNavHost.findNavController()
|
||||
|
||||
isEnabled =
|
||||
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
|
||||
queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
|
||||
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
|
||||
exploreNavController.currentDestination?.id !=
|
||||
exploreNavController.graph.startDestinationId
|
||||
}
|
||||
|
|
|
@ -175,10 +175,7 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
|
|||
navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the currently displayed [Album].
|
||||
* @param album The new [Album] to display. Null if there is no longer one.
|
||||
*/
|
||||
|
||||
private fun updateAlbum(album: Album?) {
|
||||
if (album == null) {
|
||||
// Album we were showing no longer exists.
|
||||
|
@ -189,12 +186,6 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
|
|||
requireBinding().detailToolbar.title = album.resolveName(requireContext())
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current playback state in the context of the currently displayed [Album].
|
||||
* @param song The current [Song] playing.
|
||||
* @param parent The current [MusicParent] playing, null if all songs.
|
||||
* @param isPlaying Whether playback is ongoing or paused.
|
||||
*/
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
|
||||
detailAdapter.setPlayingItem(song, isPlaying)
|
||||
|
@ -204,10 +195,6 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a navigation event.
|
||||
* @param item The [Music] to navigate to, null if there is no item.
|
||||
*/
|
||||
private fun handleNavigation(item: Music?) {
|
||||
val binding = requireBinding()
|
||||
when (item) {
|
||||
|
@ -216,7 +203,7 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
|
|||
is Song -> {
|
||||
if (unlikelyToBeNull(detailModel.currentAlbum.value) == item.album) {
|
||||
logD("Navigating to a song in this album")
|
||||
scrollToItem(item)
|
||||
scrollToAlbumSong(item)
|
||||
navModel.finishExploreNavigation()
|
||||
} else {
|
||||
logD("Navigating to another album")
|
||||
|
@ -250,11 +237,7 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to a [Song] within the [Album] view, assuming that it is present.
|
||||
* @param song The song to try to scroll to.
|
||||
*/
|
||||
private fun scrollToItem(song: Song) {
|
||||
private fun scrollToAlbumSong(song: Song) {
|
||||
// Calculate where the item for the currently played song is
|
||||
val pos = detailModel.albumList.value.indexOf(song)
|
||||
|
||||
|
@ -293,10 +276,6 @@ class AlbumDetailFragment : ListFragment<FragmentDetailBinding>(), AlbumDetailAd
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current item selection.
|
||||
* @param selected The list of selected items.
|
||||
*/
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
detailAdapter.setSelectedItems(selected)
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
|
|
|
@ -179,10 +179,6 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapte
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the currently displayed [Artist]
|
||||
* @param artist The new [Artist] to display. Null if there is no longer one.
|
||||
*/
|
||||
private fun updateItem(artist: Artist?) {
|
||||
if (artist == null) {
|
||||
// Artist we were showing no longer exists.
|
||||
|
@ -193,17 +189,11 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapte
|
|||
requireBinding().detailToolbar.title = artist.resolveName(requireContext())
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current playback state in the context of the currently displayed [Artist].
|
||||
* @param song The current [Song] playing.
|
||||
* @param parent The current [MusicParent] playing, null if all songs.
|
||||
* @param isPlaying Whether playback is ongoing or paused.
|
||||
*/
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
val currentArtist = unlikelyToBeNull(detailModel.currentArtist.value)
|
||||
val playingItem =
|
||||
when (parent) {
|
||||
// Always highlight a playing album from this artist.
|
||||
// Always highlight a playing album if it's from this artist.
|
||||
is Album -> parent
|
||||
// If the parent is the artist itself, use the currently playing song.
|
||||
currentArtist -> song
|
||||
|
@ -214,10 +204,6 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapte
|
|||
detailAdapter.setPlayingItem(playingItem, isPlaying)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a navigation event.
|
||||
* @param item The [Music] to navigate to, null if there is no item.
|
||||
*/
|
||||
private fun handleNavigation(item: Music?) {
|
||||
val binding = requireBinding()
|
||||
|
||||
|
@ -253,10 +239,6 @@ class ArtistDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapte
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current item selection.
|
||||
* @param selected The list of selected items.
|
||||
*/
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
detailAdapter.setSelectedItems(selected)
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
|
|
|
@ -61,8 +61,7 @@ class DetailViewModel(application: Application) :
|
|||
|
||||
private val _currentSong = MutableStateFlow<DetailSong?>(null)
|
||||
/**
|
||||
* The current [Song] that should be displayed in the [Song] detail view. Null if there
|
||||
* is no [Song].
|
||||
* The current [DetailSong] to display. Null if there is nothing to show.
|
||||
* TODO: De-couple Song and Properties?
|
||||
*/
|
||||
val currentSong: StateFlow<DetailSong?>
|
||||
|
@ -71,23 +70,16 @@ class DetailViewModel(application: Application) :
|
|||
// --- ALBUM ---
|
||||
|
||||
private val _currentAlbum = MutableStateFlow<Album?>(null)
|
||||
/**
|
||||
* The current [Album] that should be displayed in the [Album] detail view. Null if there
|
||||
* is no [Album].
|
||||
*/
|
||||
/** The current [Album] to display. Null if there is nothing to show. */
|
||||
val currentAlbum: StateFlow<Album?>
|
||||
get() = _currentAlbum
|
||||
|
||||
private val _albumData = MutableStateFlow(listOf<Item>())
|
||||
/**
|
||||
* The current list data derived from [currentAlbum], for use in the [Album] detail view.
|
||||
*/
|
||||
private val _albumList = MutableStateFlow(listOf<Item>())
|
||||
/** The current list data derived from [currentAlbum]. */
|
||||
val albumList: StateFlow<List<Item>>
|
||||
get() = _albumData
|
||||
get() = _albumList
|
||||
|
||||
/**
|
||||
* The current [Sort] used for [Song]s in the [Album] detail view.
|
||||
*/
|
||||
/** The current [Sort] used for [Song]s in [albumList]. */
|
||||
var albumSort: Sort
|
||||
get() = settings.detailAlbumSort
|
||||
set(value) {
|
||||
|
@ -99,22 +91,15 @@ class DetailViewModel(application: Application) :
|
|||
// --- ARTIST ---
|
||||
|
||||
private val _currentArtist = MutableStateFlow<Artist?>(null)
|
||||
/**
|
||||
* The current [Artist] that should be displayed in the [Artist] detail view. Null if there
|
||||
* is no [Artist].
|
||||
*/
|
||||
/** The current [Artist] to display. Null if there is nothing to show. */
|
||||
val currentArtist: StateFlow<Artist?>
|
||||
get() = _currentArtist
|
||||
|
||||
private val _artistData = MutableStateFlow(listOf<Item>())
|
||||
/**
|
||||
* The current list derived from [currentArtist], for use in the [Artist] detail view.
|
||||
*/
|
||||
val artistList: StateFlow<List<Item>> = _artistData
|
||||
private val _artistList = MutableStateFlow(listOf<Item>())
|
||||
/** The current list derived from [currentArtist]. */
|
||||
val artistList: StateFlow<List<Item>> = _artistList
|
||||
|
||||
/**
|
||||
* The current [Sort] used for [Song]s in the [Artist] detail view.
|
||||
*/
|
||||
/** The current [Sort] used for [Song]s in [artistList]. */
|
||||
var artistSort: Sort
|
||||
get() = settings.detailArtistSort
|
||||
set(value) {
|
||||
|
@ -126,22 +111,15 @@ class DetailViewModel(application: Application) :
|
|||
// --- GENRE ---
|
||||
|
||||
private val _currentGenre = MutableStateFlow<Genre?>(null)
|
||||
/**
|
||||
* The current [Genre] that should be displayed in the [Genre] detail view. Null if there
|
||||
* is no [Genre].
|
||||
*/
|
||||
/** The current [Genre] to display. Null if there is nothing to show. */
|
||||
val currentGenre: StateFlow<Genre?>
|
||||
get() = _currentGenre
|
||||
|
||||
private val _genreData = MutableStateFlow(listOf<Item>())
|
||||
/**
|
||||
* The current list data derived from [currentGenre], for use in the [Genre] detail view.
|
||||
*/
|
||||
val genreList: StateFlow<List<Item>> = _genreData
|
||||
private val _genreList = MutableStateFlow(listOf<Item>())
|
||||
/** The current list data derived from [currentGenre]. */
|
||||
val genreList: StateFlow<List<Item>> = _genreList
|
||||
|
||||
/**
|
||||
* The current [Sort] used for [Song]s in the [Genre] detail view.
|
||||
*/
|
||||
/** The current [Sort] used for [Song]s in [genreList]. */
|
||||
var genreSort: Sort
|
||||
get() = settings.detailGenreSort
|
||||
set(value) {
|
||||
|
@ -254,14 +232,6 @@ class DetailViewModel(application: Application) :
|
|||
_currentGenre.value = requireMusic<Genre>(uid).also { refreshGenreList(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around [MusicStore.Library.find] that asserts that the returned data should
|
||||
* be valid.
|
||||
* @param T The type of music that should be found
|
||||
* @param uid The [Music.UID] of the [T] to find
|
||||
* @return A [T] with the given [Music.UID]
|
||||
* @throws IllegalStateException If nothing can be found
|
||||
*/
|
||||
private fun <T: Music> requireMusic(uid: Music.UID): T =
|
||||
requireNotNull(unlikelyToBeNull(musicStore.library).find(uid)) { "Invalid id provided" }
|
||||
|
||||
|
@ -275,20 +245,13 @@ class DetailViewModel(application: Application) :
|
|||
_currentSong.value = DetailSong(song, null)
|
||||
currentSongJob =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val info = loadSongProperties(song)
|
||||
val info = loadProperties(song)
|
||||
yield()
|
||||
_currentSong.value = DetailSong(song, info)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a new set of [DetailSong.Properties] based on the given [Song]'s file using
|
||||
* [MediaExtractor].
|
||||
* @param song The song to load the properties from.
|
||||
* @return A [DetailSong.Properties] containing the properties that could be
|
||||
* extracted from the file.
|
||||
*/
|
||||
private fun loadSongProperties(song: Song): DetailSong.Properties {
|
||||
private fun loadProperties(song: Song): DetailSong.Properties {
|
||||
// While we would use ExoPlayer to extract this information, it doesn't support
|
||||
// common data like bit rate in progressive data sources due to there being no
|
||||
// demand. Thus, we are stuck with the inferior OS-provided MediaExtractor.
|
||||
|
@ -349,10 +312,6 @@ class DetailViewModel(application: Application) :
|
|||
return DetailSong.Properties(bitrate, sampleRate, resolvedMimeType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh [albumList] to reflect the given [Album] and any [Sort] changes.
|
||||
* @param album The [Album] to create the list from.
|
||||
*/
|
||||
private fun refreshAlbumList(album: Album) {
|
||||
logD("Refreshing album data")
|
||||
val data = mutableListOf<Item>(album)
|
||||
|
@ -374,13 +333,9 @@ class DetailViewModel(application: Application) :
|
|||
data.addAll(songs)
|
||||
}
|
||||
|
||||
_albumData.value = data
|
||||
_albumList.value = data
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh [artistList] to reflect the given [Artist] and any [Sort] changes.
|
||||
* @param artist The [Artist] to create the list from.
|
||||
*/
|
||||
private fun refreshArtistList(artist: Artist) {
|
||||
logD("Refreshing artist data")
|
||||
val data = mutableListOf<Item>(artist)
|
||||
|
@ -421,13 +376,9 @@ class DetailViewModel(application: Application) :
|
|||
data.addAll(artistSort.songs(artist.songs))
|
||||
}
|
||||
|
||||
_artistData.value = data.toList()
|
||||
_artistList.value = data.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh [genreList] to reflect the given [Genre] and any [Sort] changes.
|
||||
* @param genre The [Genre] to create the list from.
|
||||
*/
|
||||
private fun refreshGenreList(genre: Genre) {
|
||||
logD("Refreshing genre data")
|
||||
val data = mutableListOf<Item>(genre)
|
||||
|
@ -436,7 +387,7 @@ class DetailViewModel(application: Application) :
|
|||
data.addAll(genre.artists)
|
||||
data.add(SortHeader(R.string.lbl_songs))
|
||||
data.addAll(genreSort.songs(genre.songs))
|
||||
_genreData.value = data
|
||||
_genreList.value = data
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -174,10 +174,6 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapter
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the currently displayed [Genre]
|
||||
* @param genre The new [Genre] to display. Null if there is no longer one.
|
||||
*/
|
||||
private fun updateItem(genre: Genre?) {
|
||||
if (genre == null) {
|
||||
// Genre we were showing no longer exists.
|
||||
|
@ -188,12 +184,6 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapter
|
|||
requireBinding().detailToolbar.title = genre.resolveName(requireContext())
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current playback state in the context of the currently displayed [Genre].
|
||||
* @param song The current [Song] playing.
|
||||
* @param parent The current [MusicParent] playing, null if all songs.
|
||||
* @param isPlaying Whether playback is ongoing or paused.
|
||||
*/
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
var item: Item? = null
|
||||
|
||||
|
@ -208,10 +198,6 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapter
|
|||
detailAdapter.setPlayingItem(item, isPlaying)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a navigation event.
|
||||
* @param item The [Music] to navigate to, null if there is no item.
|
||||
*/
|
||||
private fun handleNavigation(item: Music?) {
|
||||
when (item) {
|
||||
is Song -> {
|
||||
|
@ -236,10 +222,6 @@ class GenreDetailFragment : ListFragment<FragmentDetailBinding>(), DetailAdapter
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current item selection.
|
||||
* @param selected The list of selected items.
|
||||
*/
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
detailAdapter.setSelectedItems(selected)
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
|
|
|
@ -39,7 +39,6 @@ constructor(
|
|||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.editTextStyle
|
||||
) : TextInputEditText(context, attrs, defStyleAttr) {
|
||||
|
||||
init {
|
||||
// Enable selection, but still disable focus (i.e Keyboard opening)
|
||||
setTextIsSelectable(true)
|
||||
|
@ -50,10 +49,8 @@ constructor(
|
|||
|
||||
// Make text immutable
|
||||
override fun getFreezesText() = false
|
||||
|
||||
// Prevent editing by default
|
||||
override fun getDefaultEditable() = false
|
||||
|
||||
// Remove the movement method that allows cursor scrolling
|
||||
override fun getDefaultMovementMethod() = null
|
||||
}
|
||||
|
|
|
@ -56,10 +56,6 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
|||
collectImmediately(detailModel.currentSong, ::updateSong)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the currently displayed song.
|
||||
* @param song The [DetailViewModel.DetailSong] to display. Null if there is no longer one.
|
||||
*/
|
||||
private fun updateSong(song: DetailSong?) {
|
||||
val binding = requireBinding()
|
||||
|
||||
|
@ -71,19 +67,16 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
|||
|
||||
if (song.properties != null) {
|
||||
// Finished loading Song properties, populate and show the list of Song information.
|
||||
binding.detailLoading.isInvisible = true
|
||||
binding.detailContainer.isInvisible = false
|
||||
|
||||
val context = requireContext()
|
||||
// File name
|
||||
binding.detailFileName.setText(song.song.path.name)
|
||||
// Relative (Parent) directory
|
||||
binding.detailRelativeDir.setText(song.song.path.parent.resolveName(context))
|
||||
// Format
|
||||
binding.detailFormat.setText(song.properties.resolvedMimeType.resolveName(context))
|
||||
// Size
|
||||
binding.detailSize.setText(Formatter.formatFileSize(context, song.song.size))
|
||||
// Duration
|
||||
binding.detailDuration.setText(song.song.durationMs.formatDurationMs(true))
|
||||
|
||||
// Bit rate (if present)
|
||||
if (song.properties.bitrateKbps != null) {
|
||||
binding.detailBitrate.setText(
|
||||
getString(R.string.fmt_bitrate, song.properties.bitrateKbps))
|
||||
|
@ -91,16 +84,12 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
|||
binding.detailBitrate.setText(R.string.def_bitrate)
|
||||
}
|
||||
|
||||
// Sample rate (if present)
|
||||
if (song.properties.sampleRateHz != null) {
|
||||
binding.detailSampleRate.setText(
|
||||
getString(R.string.fmt_sample_rate, song.properties.sampleRateHz))
|
||||
} else {
|
||||
binding.detailSampleRate.setText(R.string.def_sample_rate)
|
||||
}
|
||||
|
||||
binding.detailLoading.isInvisible = true
|
||||
binding.detailContainer.isInvisible = false
|
||||
} else {
|
||||
// Loading is still on-going, don't show anything yet.
|
||||
binding.detailLoading.isInvisible = false
|
||||
|
|
|
@ -40,7 +40,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
|
||||
/**
|
||||
* An [DetailAdapter] implementing the header and sub-items for the [Album] detail view.
|
||||
* @param listener A [Listener] for list interactions.
|
||||
* @param listener A [Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) {
|
||||
|
|
|
@ -39,7 +39,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
|
||||
/**
|
||||
* A [DetailAdapter] implementing the header and sub-items for the [Artist] detail view.
|
||||
* @param listener A [DetailAdapter.Listener] for list interactions.
|
||||
* @param listener A [DetailAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) {
|
||||
|
|
|
@ -35,13 +35,13 @@ import org.oxycblt.auxio.util.inflater
|
|||
|
||||
/**
|
||||
* A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters.
|
||||
* @param callback A [Listener] for list interactions.
|
||||
* @param listener A [Listener] to bind interactions to.
|
||||
* @param itemCallback A [DiffUtil.ItemCallback] to use with [AsyncListDiffer] when updating the
|
||||
* internal list.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
abstract class DetailAdapter(
|
||||
private val callback: Listener,
|
||||
private val listener: Listener,
|
||||
itemCallback: DiffUtil.ItemCallback<Item>
|
||||
) : SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
||||
// Safe to leak this since the callback will not fire during initialization
|
||||
|
@ -67,7 +67,7 @@ abstract class DetailAdapter(
|
|||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (val item = differ.currentList[position]) {
|
||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, callback)
|
||||
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,9 +89,7 @@ abstract class DetailAdapter(
|
|||
differ.submitList(newList)
|
||||
}
|
||||
|
||||
/**
|
||||
* An extended [ExtendedListListener] for [DetailAdapter] implementations.
|
||||
*/
|
||||
/** An extended [ExtendedListListener] for [DetailAdapter] implementations. */
|
||||
interface Listener : ExtendedListListener {
|
||||
// TODO: Split off into sub-listeners if a collapsing toolbar is implemented.
|
||||
/**
|
||||
|
|
|
@ -37,7 +37,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
|
||||
/**
|
||||
* An [DetailAdapter] implementing the header and sub-items for the [Genre] detail view.
|
||||
* @param listener A [DetailAdapter.Listener] for list interactions.
|
||||
* @param listener A [DetailAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) {
|
||||
|
|
|
@ -93,7 +93,7 @@ class HomeFragment :
|
|||
// our transitions.
|
||||
val axis = savedInstanceState.getInt(KEY_LAST_TRANSITION_AXIS, -1)
|
||||
if (axis > -1) {
|
||||
initAxisTransitions(axis)
|
||||
setupAxisTransitions(axis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -199,7 +199,7 @@ class HomeFragment :
|
|||
// Handle main actions (Search, Settings, About)
|
||||
R.id.action_search -> {
|
||||
logD("Navigating to search")
|
||||
initAxisTransitions(MaterialSharedAxis.Z)
|
||||
setupAxisTransitions(MaterialSharedAxis.Z)
|
||||
findNavController().navigate(HomeFragmentDirections.actionShowSearch())
|
||||
}
|
||||
R.id.action_settings -> {
|
||||
|
@ -238,10 +238,6 @@ class HomeFragment :
|
|||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the TabLayout and [ViewPager2] to reflect the current tab configuration.
|
||||
* @param binding The [FragmentHomeBinding] to apply the tab configuration to.
|
||||
*/
|
||||
private fun setupPager(binding: FragmentHomeBinding) {
|
||||
binding.homePager.adapter = HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner)
|
||||
|
||||
|
@ -264,10 +260,6 @@ class HomeFragment :
|
|||
AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes)).attach()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the UI to reflect the current tab.
|
||||
* @param tabMode The [MusicMode] of the currently shown tab.
|
||||
*/
|
||||
private fun updateCurrentTab(tabMode: MusicMode) {
|
||||
// Update the sort options to align with those allowed by the tab
|
||||
val isVisible: (Int) -> Boolean = when (tabMode) {
|
||||
|
@ -310,10 +302,6 @@ class HomeFragment :
|
|||
requireBinding().homeAppbar.liftOnScrollTargetViewId = getTabRecyclerId(tabMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request to recreate all home tabs.
|
||||
* @param recreate Whether to recreate all tabs.
|
||||
*/
|
||||
private fun handleRecreate(recreate: Boolean) {
|
||||
if (!recreate) {
|
||||
// Nothing to do
|
||||
|
@ -328,11 +316,6 @@ class HomeFragment :
|
|||
homeModel.finishRecreate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the currently displayed [Indexer.State]
|
||||
* @param state The new [Indexer.State] to show. Null if the state is currently
|
||||
* indeterminate (Not loading or complete).
|
||||
*/
|
||||
private fun updateIndexerState(state: Indexer.State?) {
|
||||
val binding = requireBinding()
|
||||
when (state) {
|
||||
|
@ -345,11 +328,6 @@ class HomeFragment :
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the UI to display the given [Indexer.Response].
|
||||
* @param binding The [FragmentHomeBinding] whose views should be updated.
|
||||
* @param response The [Indexer.Response] to show.
|
||||
*/
|
||||
private fun setupCompleteState(binding: FragmentHomeBinding, response: Indexer.Response) {
|
||||
if (response is Indexer.Response.Ok) {
|
||||
logD("Received ok response")
|
||||
|
@ -401,11 +379,6 @@ class HomeFragment :
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the UI to display the given [Indexer.Indexing] state..
|
||||
* @param binding The [FragmentHomeBinding] whose views should be updated.
|
||||
* @param indexing The [Indexer.Indexing] state to show.
|
||||
*/
|
||||
private fun setupIndexingState(binding: FragmentHomeBinding, indexing: Indexer.Indexing) {
|
||||
// Remove all content except for the progress indicator.
|
||||
binding.homeIndexingContainer.visibility = View.VISIBLE
|
||||
|
@ -431,11 +404,6 @@ class HomeFragment :
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the FAB visibility to reflect the state of other home elements.
|
||||
* @param songs The current list of songs in the home view.
|
||||
* @param isFastScrolling Whether the user is currently fast-scrolling.
|
||||
*/
|
||||
private fun updateFab(songs: List<Song>, isFastScrolling: Boolean) {
|
||||
val binding = requireBinding()
|
||||
// If there are no songs, it's likely that the library has not been loaded, so
|
||||
|
@ -448,10 +416,6 @@ class HomeFragment :
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a navigation event.
|
||||
* @param item The [Music] to navigate to, null if there is no item.
|
||||
*/
|
||||
private fun handleNavigation(item: Music?) {
|
||||
val action =
|
||||
when (item) {
|
||||
|
@ -462,14 +426,10 @@ class HomeFragment :
|
|||
else -> return
|
||||
}
|
||||
|
||||
initAxisTransitions(MaterialSharedAxis.X)
|
||||
setupAxisTransitions(MaterialSharedAxis.X)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current item selection.
|
||||
* @param selected The list of selected items.
|
||||
*/
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
val binding = requireBinding()
|
||||
if (binding.homeSelectionToolbar.updateSelectionAmount(selected.size) &&
|
||||
|
@ -481,24 +441,7 @@ class HomeFragment :
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the RecyclerView contained by a tab.
|
||||
* @param tabMode The mode of the tab to get the ID from
|
||||
* @return The ID of the RecyclerView contained by the given tab.
|
||||
*/
|
||||
private fun getTabRecyclerId(tabMode: MusicMode) =
|
||||
when (tabMode) {
|
||||
MusicMode.SONGS -> R.id.home_song_recycler
|
||||
MusicMode.ALBUMS -> R.id.home_album_recycler
|
||||
MusicMode.ARTISTS -> R.id.home_artist_recycler
|
||||
MusicMode.GENRES -> R.id.home_genre_recycler
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up [MaterialSharedAxis] transitions.
|
||||
* @param axis The axis that the transition should occur on, such as [MaterialSharedAxis.X]
|
||||
*/
|
||||
private fun initAxisTransitions(axis: Int) {
|
||||
private fun setupAxisTransitions(axis: Int) {
|
||||
// Sanity check to avoid in-correct axis transitions
|
||||
check(axis == MaterialSharedAxis.X || axis == MaterialSharedAxis.Z) {
|
||||
"Not expecting Y axis transition"
|
||||
|
@ -510,9 +453,23 @@ class HomeFragment :
|
|||
reenterTransition = MaterialSharedAxis(axis, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the RecyclerView contained by [ViewPager2] tab represented with
|
||||
* the given [MusicMode].
|
||||
* @param tabMode The [MusicMode] of the tab.
|
||||
* @return The ID of the RecyclerView contained by the given tab.
|
||||
*/
|
||||
private fun getTabRecyclerId(tabMode: MusicMode) =
|
||||
when (tabMode) {
|
||||
MusicMode.SONGS -> R.id.home_song_recycler
|
||||
MusicMode.ALBUMS -> R.id.home_album_recycler
|
||||
MusicMode.ARTISTS -> R.id.home_artist_recycler
|
||||
MusicMode.GENRES -> R.id.home_genre_recycler
|
||||
}
|
||||
|
||||
/**
|
||||
* [FragmentStateAdapter] implementation for the [HomeFragment]'s [ViewPager2] instance.
|
||||
* @param tabs The current tab configuration. This should define the fragments created.
|
||||
* @param tabs The current tab configuration. This will define the [Fragment]s created.
|
||||
* @param fragmentManager The [FragmentManager] required by [FragmentStateAdapter].
|
||||
* @param lifecycleOwner The [LifecycleOwner], whose Lifecycle is required by
|
||||
* [FragmentStateAdapter].
|
||||
|
|
|
@ -77,7 +77,7 @@ class HomeViewModel(application: Application) :
|
|||
* A list of [MusicMode] corresponding to the current [Tab] configuration, excluding
|
||||
* invisible [Tab]s.
|
||||
*/
|
||||
var currentTabModes: List<MusicMode> = getVisibleTabModes()
|
||||
var currentTabModes: List<MusicMode> = makeTabModes()
|
||||
private set
|
||||
|
||||
private val _currentTabMode = MutableStateFlow(currentTabModes[0])
|
||||
|
@ -133,7 +133,7 @@ class HomeViewModel(application: Application) :
|
|||
when (key) {
|
||||
context.getString(R.string.set_key_lib_tabs) -> {
|
||||
// Tabs changed, update the current tabs and set up a re-create event.
|
||||
currentTabModes = getVisibleTabModes()
|
||||
currentTabModes = makeTabModes()
|
||||
_shouldRecreate.value = true
|
||||
}
|
||||
|
||||
|
@ -155,7 +155,8 @@ class HomeViewModel(application: Application) :
|
|||
}
|
||||
|
||||
/**
|
||||
* Mark the recreation process as completed, resetting [shouldRecreate].
|
||||
* Mark the recreation process as complete.
|
||||
* @see shouldRecreate
|
||||
*/
|
||||
fun finishRecreate() {
|
||||
_shouldRecreate.value = false
|
||||
|
@ -211,9 +212,10 @@ class HomeViewModel(application: Application) :
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the [MusicMode]s of the visible [Tab]s from the [Tab] configuration.
|
||||
* @return A list of [MusicMode]s for each visible [Tab] in 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,
|
||||
* ordered in the same way as the configuration.
|
||||
*/
|
||||
private fun getVisibleTabModes() =
|
||||
private fun makeTabModes() =
|
||||
settings.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
||||
}
|
||||
|
|
|
@ -132,11 +132,6 @@ class AlbumListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRec
|
|||
openMusicMenu(anchor, R.menu.menu_album_actions, item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current playback state in the context of the current [Album] list.
|
||||
* @param parent The current [MusicParent] playing, null if all songs.
|
||||
* @param isPlaying Whether playback is ongoing or paused.
|
||||
*/
|
||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
// If an album is playing, highlight it within this adapter.
|
||||
albumAdapter.setPlayingItem(parent as? Album, isPlaying)
|
||||
|
@ -144,7 +139,7 @@ class AlbumListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRec
|
|||
|
||||
/**
|
||||
* A [SelectionIndicatorAdapter] that shows a list of [Album]s using [AlbumViewHolder].
|
||||
* @param listener An [ExtendedListListener] for list interactions.
|
||||
* @param listener An [ExtendedListListener] to bind interactions to.
|
||||
*/
|
||||
private class AlbumAdapter(private val listener: ExtendedListListener) :
|
||||
SelectionIndicatorAdapter<AlbumViewHolder>() {
|
||||
|
|
|
@ -107,11 +107,6 @@ class ArtistListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRe
|
|||
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current playback state in the context of the current [Artist] list.
|
||||
* @param parent The current [MusicParent] playing, null if all songs.
|
||||
* @param isPlaying Whether playback is ongoing or paused.
|
||||
*/
|
||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
// If an artist is playing, highlight it within this adapter.
|
||||
homeAdapter.setPlayingItem(parent as? Artist, isPlaying)
|
||||
|
@ -119,7 +114,7 @@ class ArtistListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRe
|
|||
|
||||
/**
|
||||
* A [SelectionIndicatorAdapter] that shows a list of [Artist]s using [ArtistViewHolder].
|
||||
* @param listener An [ExtendedListListener] for list interactions.
|
||||
* @param listener An [ExtendedListListener] to bind interactions to.
|
||||
*/
|
||||
private class ArtistAdapter(private val listener: ExtendedListListener) :
|
||||
SelectionIndicatorAdapter<ArtistViewHolder>() {
|
||||
|
|
|
@ -106,11 +106,6 @@ class GenreListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRec
|
|||
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current playback state in the context of the current [Genre] list.
|
||||
* @param parent The current [MusicParent] playing, null if all songs.
|
||||
* @param isPlaying Whether playback is ongoing or paused.
|
||||
*/
|
||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
// If a genre is playing, highlight it within this adapter.
|
||||
homeAdapter.setPlayingItem(parent as? Genre, isPlaying)
|
||||
|
@ -118,7 +113,7 @@ class GenreListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRec
|
|||
|
||||
/**
|
||||
* A [SelectionIndicatorAdapter] that shows a list of [Genre]s using [GenreViewHolder].
|
||||
* @param listener An [ExtendedListListener] for list interactions.
|
||||
* @param listener An [ExtendedListListener] to bind interactions to.
|
||||
*/
|
||||
private class GenreAdapter(private val listener: ExtendedListListener) :
|
||||
SelectionIndicatorAdapter<GenreViewHolder>() {
|
||||
|
|
|
@ -142,11 +142,6 @@ class SongListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecy
|
|||
openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current playback state in the context of the current [Song] list.
|
||||
* @param parent The current [MusicParent] playing, null if all songs.
|
||||
* @param isPlaying Whether playback is ongoing or paused.
|
||||
*/
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent == null) {
|
||||
homeAdapter.setPlayingItem(song, isPlaying)
|
||||
|
@ -158,7 +153,7 @@ class SongListFragment : ListFragment<FragmentHomeListBinding>(), FastScrollRecy
|
|||
|
||||
/**
|
||||
* A [SelectionIndicatorAdapter] that shows a list of [Song]s using [SongViewHolder].
|
||||
* @param listener An [ExtendedListListener] for list interactions.
|
||||
* @param listener An [ExtendedListListener] to bind interactions to.
|
||||
*/
|
||||
private class SongAdapter(private val listener: ExtendedListListener) :
|
||||
SelectionIndicatorAdapter<SongViewHolder>() {
|
||||
|
|
|
@ -54,20 +54,17 @@ sealed class Tab(open val mode: MusicMode) {
|
|||
// Where V is a bit representing the visibility and T is a 3-bit integer representing the
|
||||
// MusicMode for this tab.
|
||||
|
||||
/**
|
||||
* The length a well-formed tab sequence should be
|
||||
*/
|
||||
/** The length a well-formed tab sequence should be. */
|
||||
private const val SEQUENCE_LEN = 4
|
||||
|
||||
/**
|
||||
* The default tab sequence, in integer form.
|
||||
* This will be SONGS, ALBUMS, ARTISTS, GENRES, PLAYLISTS.
|
||||
* This represents a set of four visible tabs ordered as "Song", "Album", "Artist", and
|
||||
* "Genre".
|
||||
*/
|
||||
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100
|
||||
|
||||
/**
|
||||
* Maps between the integer code in the tab sequence and the actual [MusicMode] instance.
|
||||
*/
|
||||
/** Maps between the integer code in the tab sequence and it's [MusicMode]. */
|
||||
private val MODE_TABLE =
|
||||
arrayOf(MusicMode.SONGS, MusicMode.ALBUMS, MusicMode.ARTISTS, MusicMode.GENRES)
|
||||
|
||||
|
@ -82,7 +79,6 @@ sealed class Tab(open val mode: MusicMode) {
|
|||
|
||||
var sequence = 0b0100
|
||||
var shift = SEQUENCE_LEN * 4
|
||||
|
||||
for (tab in distinct) {
|
||||
val bin =
|
||||
when (tab) {
|
||||
|
|
|
@ -33,34 +33,12 @@ import org.oxycblt.auxio.util.inflater
|
|||
* @param listener A [Listener] for tab interactions.
|
||||
*/
|
||||
class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewHolder>() {
|
||||
/**
|
||||
* A listener for interactions specific to tab configuration.
|
||||
*/
|
||||
interface Listener {
|
||||
/**
|
||||
* Called when a tab is clicked, requesting that the visibility should be inverted
|
||||
* (i.e Visible -> Invisible and vice versa).
|
||||
* @param tabMode The [MusicMode] of the tab clicked.
|
||||
*/
|
||||
fun onToggleVisibility(tabMode: MusicMode)
|
||||
|
||||
/**
|
||||
* Called when the drag handle is pressed, requesting that a drag should be started.
|
||||
* @param viewHolder The [RecyclerView.ViewHolder] to start dragging.
|
||||
*/
|
||||
fun onPickUpTab(viewHolder: RecyclerView.ViewHolder)
|
||||
}
|
||||
|
||||
/**
|
||||
* The current array of [Tab]s.
|
||||
*/
|
||||
/** The current array of [Tab]s. */
|
||||
var tabs = arrayOf<Tab>()
|
||||
private set
|
||||
|
||||
override fun getItemCount() = tabs.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.new(parent)
|
||||
|
||||
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
|
||||
holder.bind(tabs[position], listener)
|
||||
}
|
||||
|
@ -86,7 +64,7 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
|
|||
}
|
||||
|
||||
/**
|
||||
* Swap two tabs with eachother.
|
||||
* Swap two tabs with each other.
|
||||
* @param a The position of the first tab to swap.
|
||||
* @param b The position of the second tab to swap.
|
||||
*/
|
||||
|
@ -97,6 +75,22 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
|
|||
notifyItemMoved(a, b)
|
||||
}
|
||||
|
||||
/** A listener for interactions specific to tab configuration. */
|
||||
interface Listener {
|
||||
/**
|
||||
* Called when a tab is clicked, requesting that the visibility should be inverted
|
||||
* (i.e Visible -> Invisible and vice versa).
|
||||
* @param tabMode The [MusicMode] of the tab clicked.
|
||||
*/
|
||||
fun onToggleVisibility(tabMode: MusicMode)
|
||||
|
||||
/**
|
||||
* Called when the drag handle is pressed, requesting that a drag should be started.
|
||||
* @param viewHolder The [RecyclerView.ViewHolder] to start dragging.
|
||||
*/
|
||||
fun onPickUpTab(viewHolder: RecyclerView.ViewHolder)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val PAYLOAD_TAB_CHANGED = Any()
|
||||
}
|
||||
|
|
|
@ -109,6 +109,6 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS"
|
||||
private const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,14 +39,10 @@ import org.oxycblt.auxio.music.Song
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
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)
|
||||
|
||||
/**
|
||||
* The target that will recieve the requested [Bitmap].
|
||||
*/
|
||||
/** The target that will receive the requested [Bitmap]. */
|
||||
interface Target {
|
||||
/**
|
||||
* Configure the [ImageRequest.Builder] to enable [Target]-specific configuration.
|
||||
|
@ -65,11 +61,7 @@ class BitmapProvider(private val context: Context) {
|
|||
}
|
||||
|
||||
private var currentRequest: Request? = null
|
||||
// Keeps track of the current image request we are on. If the stored handle in an
|
||||
// ImageRequest is still equal to this, it means that the request has not been
|
||||
// superceded by a new one.
|
||||
private var currentHandle = 0L
|
||||
private var handleLock = Any()
|
||||
|
||||
/** If this provider is currently attempting to load something. */
|
||||
val isBusy: Boolean
|
||||
|
@ -82,13 +74,12 @@ class BitmapProvider(private val context: Context) {
|
|||
*/
|
||||
@Synchronized
|
||||
fun load(song: Song, target: Target) {
|
||||
// Increment the handle, indicating a newer request being created.
|
||||
val handle = synchronized(handleLock) { ++currentHandle }
|
||||
// Be even safer and cancel the previous request.
|
||||
// Increment the handle, indicating a newer request has been created
|
||||
val handle = ++currentHandle
|
||||
currentRequest?.run { disposable.dispose() }
|
||||
currentRequest = null
|
||||
|
||||
val request =
|
||||
val imageRequest =
|
||||
target.onConfigRequest(
|
||||
ImageRequest.Builder(context)
|
||||
.data(song)
|
||||
|
@ -99,31 +90,32 @@ class BitmapProvider(private val context: Context) {
|
|||
// callback.
|
||||
.target(
|
||||
onSuccess = {
|
||||
synchronized(handleLock) {
|
||||
synchronized(this) {
|
||||
if (currentHandle == handle) {
|
||||
// Still the active request, deliver it to the target.
|
||||
// Has not been superceded by a new request, can deliver
|
||||
// this result.
|
||||
target.onCompleted(it.toBitmap())
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
synchronized(handleLock) {
|
||||
synchronized(this) {
|
||||
if (currentHandle == handle) {
|
||||
// Still the active request, deliver it to the target.
|
||||
// Has not been superceded by a new request, can deliver
|
||||
// this result.
|
||||
target.onCompleted(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
currentRequest = Request(context.imageLoader.enqueue(request.build()), target)
|
||||
currentRequest = Request(context.imageLoader.enqueue(imageRequest.build()), target)
|
||||
}
|
||||
|
||||
/**
|
||||
* Release this instance. Run this when the object is no longer used to prevent
|
||||
* stray loading callbacks.
|
||||
* Release this instance, cancelling any currently running operations.
|
||||
*/
|
||||
@Synchronized
|
||||
fun release() {
|
||||
synchronized(handleLock) { ++currentHandle }
|
||||
++currentHandle
|
||||
currentRequest?.run { disposable.dispose() }
|
||||
currentRequest = null
|
||||
}
|
||||
|
|
|
@ -24,17 +24,11 @@ import org.oxycblt.auxio.IntegerTable
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
enum class CoverMode {
|
||||
/**
|
||||
* Do not load album covers ("Off").
|
||||
*/
|
||||
/** Do not load album covers ("Off"). */
|
||||
OFF,
|
||||
/**
|
||||
* Load covers from the fast, but lower-quality media store database ("Fast").
|
||||
*/
|
||||
/** Load covers from the fast, but lower-quality media store database ("Fast"). */
|
||||
MEDIA_STORE,
|
||||
/**
|
||||
* Load high-quality covers directly from music files ("Quality").
|
||||
*/
|
||||
/** Load high-quality covers directly from music files ("Quality"). */
|
||||
QUALITY;
|
||||
|
||||
/**
|
||||
|
|
|
@ -65,8 +65,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
private val cornerRadius: Float
|
||||
|
||||
init {
|
||||
// Android wants you to make separate attributes for each view type, but will
|
||||
// then throw an error if you do because of duplicate attribute names.
|
||||
// Obtain some StyledImageView attributes to use later when theming the cusotm view.
|
||||
@SuppressLint("CustomViewStyleable")
|
||||
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
|
||||
// Keep track of our corner radius so that we can apply the same attributes to the custom
|
||||
|
@ -123,7 +122,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
// Initialize each component before this view is drawn.
|
||||
invalidateAlpha()
|
||||
invalidateImageAlpha()
|
||||
invalidatePlayingIndicator()
|
||||
invalidateSelectionIndicator()
|
||||
}
|
||||
|
@ -135,13 +134,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
|
||||
override fun setEnabled(enabled: Boolean) {
|
||||
super.setEnabled(enabled)
|
||||
invalidateAlpha()
|
||||
invalidateImageAlpha()
|
||||
invalidatePlayingIndicator()
|
||||
}
|
||||
|
||||
override fun setSelected(selected: Boolean) {
|
||||
super.setSelected(selected)
|
||||
invalidateAlpha()
|
||||
invalidateImageAlpha()
|
||||
invalidatePlayingIndicator()
|
||||
}
|
||||
|
||||
|
@ -185,18 +184,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
playbackIndicatorView.isPlaying = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the overall opacity of this view.
|
||||
*/
|
||||
private fun invalidateAlpha() {
|
||||
private fun invalidateImageAlpha() {
|
||||
// If this view is disabled, show it at half-opacity, *unless* it is also marked
|
||||
// as playing, in which we still want to show it at full-opacity.
|
||||
alpha = if (isSelected || isEnabled) 1f else 0.5f
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the view's playing ([isSelected]) indicator.
|
||||
*/
|
||||
private fun invalidatePlayingIndicator() {
|
||||
if (isSelected) {
|
||||
// View is "selected" (actually marked as playing), so show the playing indicator
|
||||
|
@ -213,22 +206,18 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the view's selection ([isActivated]) indicator, animating it from invisible
|
||||
* to visible (or vice versa).
|
||||
*/
|
||||
private fun invalidateSelectionIndicator() {
|
||||
// Set up a target transition for the selection indicator.
|
||||
val targetAlpha: Float
|
||||
val targetDuration: Long
|
||||
|
||||
if (isActivated) {
|
||||
// Activated -> Show selection indicator
|
||||
// View is "activated" (i.e marked as selected), so show the selection indicator.
|
||||
targetAlpha = 1f
|
||||
targetDuration =
|
||||
context.getInteger(R.integer.anim_fade_enter_duration).toLong()
|
||||
} else {
|
||||
// Activated -> Hide selection indicator.
|
||||
// View is not "activated", hide the selection indicator.
|
||||
targetAlpha = 0f
|
||||
targetDuration =
|
||||
context.getInteger(R.integer.anim_fade_exit_duration).toLong()
|
||||
|
|
|
@ -45,18 +45,13 @@ class PlaybackIndicatorView
|
|||
@JvmOverloads
|
||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||
AppCompatImageView(context, attrs, defStyleAttr) {
|
||||
// The playing drawable will cycle through an active equalizer animation.
|
||||
private val playingIndicatorDrawable =
|
||||
context.getDrawableCompat(R.drawable.ic_playing_indicator_24) as AnimationDrawable
|
||||
// The paused drawable will be a static drawable of an inactive equalizer.
|
||||
private val pausedIndicatorDrawable =
|
||||
context.getDrawableCompat(R.drawable.ic_paused_indicator_24)
|
||||
|
||||
// Required transformation matrices for the drawables.
|
||||
private val indicatorMatrix = Matrix()
|
||||
private val indicatorMatrixSrc = RectF()
|
||||
private val indicatorMatrixDst = RectF()
|
||||
|
||||
private val settings = Settings(context)
|
||||
|
||||
/**
|
||||
|
|
|
@ -66,17 +66,13 @@ private constructor(private val context: Context, private val album: Album) : Fe
|
|||
dataSource = DataSource.DISK)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [Fetcher.Factory] implementation that works with [Song]s.
|
||||
*/
|
||||
/** A [Fetcher.Factory] implementation that works with [Song]s.*/
|
||||
class SongFactory : Fetcher.Factory<Song> {
|
||||
override fun create(data: Song, options: Options, imageLoader: ImageLoader) =
|
||||
AlbumCoverFetcher(options.context, data.album)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [Fetcher.Factory] implementation that works with [Album]s.
|
||||
*/
|
||||
/** A [Fetcher.Factory] implementation that works with [Album]s. */
|
||||
class AlbumFactory : Fetcher.Factory<Album> {
|
||||
override fun create(data: Album, options: Options, imageLoader: ImageLoader) =
|
||||
AlbumCoverFetcher(options.context, data)
|
||||
|
@ -100,9 +96,7 @@ private constructor(
|
|||
return Images.createMosaic(context, results, size)
|
||||
}
|
||||
|
||||
/**
|
||||
* [Fetcher.Factory] implementation.
|
||||
*/
|
||||
/** [Fetcher.Factory] implementation. */
|
||||
class Factory : Fetcher.Factory<Artist> {
|
||||
override fun create(data: Artist, options: Options, imageLoader: ImageLoader) =
|
||||
ArtistImageFetcher(options.context, options.size, data)
|
||||
|
@ -124,6 +118,7 @@ private constructor(
|
|||
return Images.createMosaic(context, results, size)
|
||||
}
|
||||
|
||||
/** [Fetcher.Factory] implementation. */
|
||||
class Factory : Fetcher.Factory<Genre> {
|
||||
override fun create(data: Genre, options: Options, imageLoader: ImageLoader) =
|
||||
GenreImageFetcher(options.context, options.size, data)
|
||||
|
|
|
@ -26,11 +26,10 @@ import coil.transition.Transition
|
|||
import coil.transition.TransitionTarget
|
||||
|
||||
/**
|
||||
* A copy of [CrossfadeTransition.Factory] that applies a transition to error results. You know.
|
||||
* Like they used to.
|
||||
* A copy of [CrossfadeTransition.Factory] that also applies a transition to error results.
|
||||
* @author Coil Team, Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class ErrorCrossfadeTransitionFractory : Transition.Factory {
|
||||
class ErrorCrossfadeTransitionFactory : Transition.Factory {
|
||||
override fun create(target: TransitionTarget, result: ImageResult): Transition {
|
||||
// Don't animate if the request was fulfilled by the memory cache.
|
||||
if (result is SuccessResult && result.dataSource == DataSource.MEMORY_CACHE) {
|
|
@ -44,9 +44,7 @@ object Images {
|
|||
}
|
||||
}
|
||||
|
||||
// Use whatever size coil gives us to create the mosaic, rounding it to even so that we
|
||||
// get a symmetrical mosaic [and to prevent bugs]. If there is no size, default to a
|
||||
// 512x512 mosaic.
|
||||
// Use whatever size coil gives us to create the mosaic.
|
||||
val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize())
|
||||
val mosaicFrameSize =
|
||||
Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2))
|
||||
|
@ -65,16 +63,13 @@ object Images {
|
|||
break
|
||||
}
|
||||
|
||||
// Run the bitmap through a transform to make sure it's a square of the desired
|
||||
// resolution.
|
||||
// Run the bitmap through a transform to reflect the configuration of other images.
|
||||
val bitmap =
|
||||
SquareFrameTransform.INSTANCE.transform(
|
||||
BitmapFactory.decodeStream(stream), mosaicFrameSize)
|
||||
|
||||
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
||||
|
||||
x += bitmap.width
|
||||
|
||||
if (x == mosaicSize.width) {
|
||||
x = 0
|
||||
y += bitmap.height
|
||||
|
@ -90,6 +85,11 @@ object Images {
|
|||
dataSource = DataSource.DISK)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an image dimension suitable to create a mosaic with.
|
||||
* @return A pixel dimension derived from the given [Dimension] that will always be even,
|
||||
* allowing it to be sub-divided.
|
||||
*/
|
||||
private fun Dimension.mosaicSize(): Int {
|
||||
val size = pxOrElse { 512 }
|
||||
return if (size.mod(2) > 0) size + 1 else size
|
||||
|
|
|
@ -50,9 +50,7 @@ class SquareFrameTransform : Transformation {
|
|||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* A shared instance that can be re-used.
|
||||
*/
|
||||
/** A re-usable instance. */
|
||||
val INSTANCE = SquareFrameTransform()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Extende
|
|||
* closed 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 menuRes The resource of the menu to load.
|
||||
* @param song The [Artist] to create the menu for.
|
||||
* @param album The [Album] to create the menu for.
|
||||
*/
|
||||
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) {
|
||||
logD("Launching new album menu: ${album.rawName}")
|
||||
|
@ -148,7 +148,7 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Extende
|
|||
* closed 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 menuRes The resource of the menu to load.
|
||||
* @param song The [Artist] to create the menu for.
|
||||
* @param artist The [Artist] to create the menu for.
|
||||
*/
|
||||
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) {
|
||||
logD("Launching new artist menu: ${artist.rawName}")
|
||||
|
@ -181,7 +181,7 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Extende
|
|||
* closed 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 menuRes The resource of the menu to load.
|
||||
* @param song The [Genre] to create the menu for.
|
||||
* @param genre The [Genre] to create the menu for.
|
||||
*/
|
||||
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) {
|
||||
logD("Launching new genre menu: ${genre.rawName}")
|
||||
|
@ -209,12 +209,6 @@ abstract class ListFragment<VB : ViewBinding> : SelectionFragment<VB>(), Extende
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internally create a menu for a [Music] item.
|
||||
* @param anchor The [View] to anchor the menu to.
|
||||
* @param menuRes The resource of the menu to load.
|
||||
* @param onMenuItemClick A callback for when a [MenuItem] is selected.
|
||||
*/
|
||||
private fun openMusicMenuImpl(
|
||||
anchor: View,
|
||||
@MenuRes menuRes: Int,
|
||||
|
|
|
@ -38,19 +38,6 @@ open class AuxioRecyclerView
|
|||
@JvmOverloads
|
||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||
RecyclerView(context, attrs, defStyleAttr) {
|
||||
/**
|
||||
* An adapter-specific hook to [GridLayoutManager.SpanSizeLookup].
|
||||
*/
|
||||
interface SpanSizeLookup {
|
||||
/**
|
||||
* Get if the item at a position takes up the whole width of the [RecyclerView] or not.
|
||||
* @param position The position of the item.
|
||||
* @return true if the item is full-width, false otherwise.
|
||||
*/
|
||||
fun isItemFullWidth(position: Int): Boolean
|
||||
}
|
||||
|
||||
// Keep track of the layout-defined bottom padding so we can re-apply it when applying insets.
|
||||
private val initialPaddingBottom = paddingBottom
|
||||
|
||||
init {
|
||||
|
@ -92,4 +79,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** An [RecyclerView.Adapter]-specific hook to [GridLayoutManager.SpanSizeLookup]. */
|
||||
interface SpanSizeLookup {
|
||||
/**
|
||||
* Get if the item at a position takes up the whole width of the [RecyclerView] or not.
|
||||
* @param position The position of the item.
|
||||
* @return true if the item is full-width, false otherwise.
|
||||
*/
|
||||
fun isItemFullWidth(position: Int): Boolean
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,19 +40,6 @@ class DialogRecyclerView
|
|||
@JvmOverloads
|
||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||
RecyclerView(context, attrs, defStyleAttr) {
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that implements dialog-specific fixes.
|
||||
*/
|
||||
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
||||
init {
|
||||
// ViewHolders are not automatically full-width in dialogs, manually resize
|
||||
// them to be as such.
|
||||
root.layoutParams =
|
||||
LayoutParams(
|
||||
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
}
|
||||
|
||||
private val topDivider = MaterialDivider(context)
|
||||
private val bottomDivider = MaterialDivider(context)
|
||||
private val spacingMedium = context.getDimenPixels(R.dimen.spacing_medium)
|
||||
|
@ -90,10 +77,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
invalidateDividers()
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure a [divider] with the equivalent of match_parent and wrap_content.
|
||||
* @param divider The divider to measure.
|
||||
*/
|
||||
private fun measureDivider(divider: MaterialDivider) {
|
||||
val widthMeasureSpec =
|
||||
ViewGroup.getChildMeasureSpec(
|
||||
|
@ -108,9 +91,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
divider.measure(widthMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the visibility of both dividers.
|
||||
*/
|
||||
private fun invalidateDividers() {
|
||||
val lmm = layoutManager as LinearLayoutManager
|
||||
// Top divider should only be visible when the first item has gone off-screen.
|
||||
|
@ -119,5 +99,18 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
bottomDivider.isInvisible =
|
||||
lmm.findLastCompletelyVisibleItemPosition() == (lmm.itemCount - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that implements dialog-specific fixes.
|
||||
*/
|
||||
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
||||
init {
|
||||
// ViewHolders are not automatically full-width in dialogs, manually resize
|
||||
// them to be as such.
|
||||
root.layoutParams =
|
||||
LayoutParams(
|
||||
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,24 +28,10 @@ import org.oxycblt.auxio.util.logW
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that can display a playing indicator.
|
||||
*/
|
||||
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
||||
/**
|
||||
* Update the playing indicator within this [RecyclerView.ViewHolder].
|
||||
* @param isActive True if this item is playing, false otherwise.
|
||||
* @param isPlaying True if playback is ongoing, false if paused. If this
|
||||
* is true, [isActive] will also be true.
|
||||
*/
|
||||
abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean)
|
||||
}
|
||||
|
||||
// There are actually two states for this adapter:
|
||||
// This is sub-divided into two states:
|
||||
// - The currently playing item, which is usually marked as "selected" and becomes accented.
|
||||
// - Whether playback is ongoing, which corresponds to whether the item's ImageGroup is
|
||||
// displaying
|
||||
// marked as "playing" or not.
|
||||
private var currentItem: Item? = null
|
||||
private var isPlaying = false
|
||||
|
||||
|
@ -119,6 +105,19 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [RecyclerView.ViewHolder] that can display a playing indicator.
|
||||
*/
|
||||
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
|
||||
/**
|
||||
* Update the playing indicator within this [RecyclerView.ViewHolder].
|
||||
* @param isActive True if this item is playing, false otherwise.
|
||||
* @param isPlaying True if playback is ongoing, false if paused. If this
|
||||
* is true, [isActive] will also be true.
|
||||
*/
|
||||
abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val PAYLOAD_PLAYING_INDICATOR_CHANGED = Any()
|
||||
}
|
||||
|
|
|
@ -28,17 +28,6 @@ import org.oxycblt.auxio.music.Music
|
|||
*/
|
||||
abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
|
||||
PlayingIndicatorAdapter<VH>() {
|
||||
/**
|
||||
* A [PlayingIndicatorAdapter.ViewHolder] that can display a selection indicator.
|
||||
*/
|
||||
abstract class ViewHolder(root: View) : PlayingIndicatorAdapter.ViewHolder(root) {
|
||||
/**
|
||||
* Update the selection indicator within this [PlayingIndicatorAdapter.ViewHolder].
|
||||
* @param isSelected Whether this [PlayingIndicatorAdapter.ViewHolder] is selected.
|
||||
*/
|
||||
abstract fun updateSelectionIndicator(isSelected: Boolean)
|
||||
}
|
||||
|
||||
private var selectedItems = setOf<Music>()
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
|
||||
|
@ -79,6 +68,17 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PlayingIndicatorAdapter.ViewHolder] that can display a selection indicator.
|
||||
*/
|
||||
abstract class ViewHolder(root: View) : PlayingIndicatorAdapter.ViewHolder(root) {
|
||||
/**
|
||||
* Update the selection indicator within this [PlayingIndicatorAdapter.ViewHolder].
|
||||
* @param isSelected Whether this [PlayingIndicatorAdapter.ViewHolder] is selected.
|
||||
*/
|
||||
abstract fun updateSelectionIndicator(isSelected: Boolean)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val PAYLOAD_SELECTION_INDICATOR_CHANGED = Any()
|
||||
}
|
||||
|
|
|
@ -113,6 +113,7 @@ class SyncListDiffer<T>(
|
|||
/**
|
||||
* Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only
|
||||
* use it if the changes are trivial.
|
||||
* @param newList The list to update to.
|
||||
*/
|
||||
fun submitList(newList: List<T>) {
|
||||
if (newList == currentList) {
|
||||
|
@ -126,6 +127,7 @@ class SyncListDiffer<T>(
|
|||
/**
|
||||
* Replace this list with a new list. This is good for large diffs that are too slow to
|
||||
* update synchronously, but too chaotic to update asynchronously.
|
||||
* @param newList The list to update to.
|
||||
*/
|
||||
fun replaceList(newList: List<T>) {
|
||||
if (newList == currentList) {
|
||||
|
|
|
@ -35,7 +35,7 @@ import org.oxycblt.auxio.util.getPlural
|
|||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
/**
|
||||
* A basic [RecyclerView.ViewHolder] that displays a [Song]. Use [new] to create an instance.
|
||||
* A [RecyclerView.ViewHolder] that displays a [Song]. Use [new] to create an instance.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||
|
@ -82,7 +82,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
|||
}
|
||||
|
||||
/**
|
||||
* A basic [RecyclerView.ViewHolder] that displays a [Album]. Use [new] to create an instance.
|
||||
* A [RecyclerView.ViewHolder] that displays a [Album]. Use [new] to create an instance.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class AlbumViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||
|
@ -131,7 +131,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
|
|||
}
|
||||
|
||||
/**
|
||||
* A basic [RecyclerView.ViewHolder] that displays a [Artist]. Use [new] to create an instance.
|
||||
* A [RecyclerView.ViewHolder] that displays a [Artist]. Use [new] to create an instance.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class ArtistViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||
|
@ -189,7 +189,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
|||
}
|
||||
|
||||
/**
|
||||
* A basic [RecyclerView.ViewHolder] that displays a [Genre]. Use [new] to create an instance.
|
||||
* A [RecyclerView.ViewHolder] that displays a [Genre]. Use [new] to create an instance.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class GenreViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||
|
@ -240,7 +240,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
|
|||
}
|
||||
|
||||
/**
|
||||
* A basic [RecyclerView.ViewHolder] that displays a [Header]. Use [new] to create an instance.
|
||||
* A [RecyclerView.ViewHolder] that displays a [Header]. Use [new] to create an instance.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
|
||||
|
|
|
@ -38,9 +38,7 @@ class SelectionToolbarOverlay
|
|||
@JvmOverloads
|
||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||
FrameLayout(context, attrs, defStyleAttr) {
|
||||
// This will be populated after the inflation completes.
|
||||
private lateinit var innerToolbar: MaterialToolbar
|
||||
// The selection toolbar will be overlaid over the inner toolbar when shown.
|
||||
private val selectionToolbar =
|
||||
MaterialToolbar(context).apply {
|
||||
setNavigationIcon(R.drawable.ic_close_24)
|
||||
|
@ -50,7 +48,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
isInvisible = true
|
||||
}
|
||||
}
|
||||
// Animator to handle selection visibility animations
|
||||
private var fadeThroughAnimator: ValueAnimator? = null
|
||||
|
||||
override fun onFinishInflate() {
|
||||
|
@ -61,7 +58,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
}
|
||||
// The inner toolbar should be the first child.
|
||||
innerToolbar = getChildAt(0) as MaterialToolbar
|
||||
// Now layer the selection toolbar on top.
|
||||
// Selection toolbar should appear on top of the inner toolbar.
|
||||
addView(selectionToolbar)
|
||||
}
|
||||
|
||||
|
@ -69,6 +66,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
* Set an OnClickListener for when the "cancel" button in the selection [MaterialToolbar] is
|
||||
* pressed.
|
||||
* @param listener The OnClickListener to respond to this interaction.
|
||||
* @see MaterialToolbar.setNavigationOnClickListener
|
||||
*/
|
||||
fun setOnSelectionCancelListener(listener: OnClickListener) {
|
||||
selectionToolbar.setNavigationOnClickListener(listener)
|
||||
|
@ -78,6 +76,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
* Set an [OnMenuItemClickListener] for when a MenuItem is selected from the selection
|
||||
* [MaterialToolbar].
|
||||
* @param listener The [OnMenuItemClickListener] to respond to this interaction.
|
||||
* @see MaterialToolbar.setOnMenuItemClickListener
|
||||
*/
|
||||
fun setOnMenuItemClickListener(listener: OnMenuItemClickListener?) {
|
||||
selectionToolbar.setOnMenuItemClickListener(listener)
|
||||
|
@ -134,7 +133,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
if (!isLaidOut) {
|
||||
// Not laid out, just change it immediately while are not shown to the user.
|
||||
// This is an initialization, so we return false despite changing.
|
||||
changeToolbarAlpha(targetInnerAlpha)
|
||||
setToolbarsAlpha(targetInnerAlpha)
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -146,7 +145,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
fadeThroughAnimator =
|
||||
ValueAnimator.ofFloat(innerToolbar.alpha, targetInnerAlpha).apply {
|
||||
duration = targetDuration
|
||||
addUpdateListener { changeToolbarAlpha(it.animatedValue as Float) }
|
||||
addUpdateListener { setToolbarsAlpha(it.animatedValue as Float) }
|
||||
start()
|
||||
}
|
||||
|
||||
|
@ -158,7 +157,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
* @param innerAlpha The opacity of the inner [MaterialToolbar]. This will map to the
|
||||
* inverse opacity of the selection [MaterialToolbar].
|
||||
*/
|
||||
private fun changeToolbarAlpha(innerAlpha: Float) {
|
||||
private fun setToolbarsAlpha(innerAlpha: Float) {
|
||||
innerToolbar.apply {
|
||||
alpha = innerAlpha
|
||||
isInvisible = innerAlpha == 0f
|
||||
|
|
|
@ -167,20 +167,13 @@ sealed class Music : Item {
|
|||
override fun toString() = "${format.namespace}:${mode.intCode.toString(16)}-$uuid"
|
||||
|
||||
/**
|
||||
* Defines the format of this [UID].
|
||||
* @param namespace The namespace that will be used in the [UID]'s string representation
|
||||
* to indicate the format.
|
||||
* Internal marker of [Music.UID] format type.
|
||||
* @param namespace Namespace to use in the [Music.UID]'s string representation.
|
||||
*/
|
||||
private enum class Format(val namespace: String) {
|
||||
/**
|
||||
* Auxio-style [UID]s derived from hash of the*non-subjective, unlikely-to-change
|
||||
* metadata.
|
||||
*/
|
||||
/** @see auxio */
|
||||
AUXIO("org.oxycblt.auxio"),
|
||||
|
||||
/**
|
||||
* Auxio-style [UID]s derived from a MusicBrainz ID.
|
||||
*/
|
||||
/** @see musicBrainz */
|
||||
MUSICBRAINZ("org.musicbrainz")
|
||||
}
|
||||
|
||||
|
@ -250,9 +243,7 @@ sealed class Music : Item {
|
|||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Cached collator instance to be used with [makeCollationKeyImpl].
|
||||
*/
|
||||
/** Cached collator instance re-used with [makeCollationKeyImpl]. */
|
||||
private val COLLATOR = Collator.getInstance().apply { strength = Collator.PRIMARY }
|
||||
}
|
||||
}
|
||||
|
@ -308,23 +299,21 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
override val collationKey = makeCollationKeyImpl()
|
||||
override fun resolveName(context: Context) = rawName
|
||||
|
||||
/**
|
||||
* The track number. Will be null if no valid track number was present in the metadata.
|
||||
*/
|
||||
/** The track number. Will be null if no valid track number was present in the metadata. */
|
||||
val track = raw.track
|
||||
/**
|
||||
* The disc number. Will be null if no valid disc number was present in the metadata.
|
||||
*/
|
||||
|
||||
/** The disc number. Will be null if no valid disc number was present in the metadata. */
|
||||
val disc = raw.disc
|
||||
/**
|
||||
* The release [Date]. Will be null if no valid date was present in the metadata.
|
||||
*/
|
||||
|
||||
/** The release [Date]. Will be null if no valid date was present in the metadata. */
|
||||
val date = raw.date
|
||||
|
||||
/**
|
||||
* The URI to the audio file that this instance was created from. This can be used to
|
||||
* access the audio file in a way that is scoped-storage-safe.
|
||||
*/
|
||||
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
|
||||
* favored instead for accessing the audio file.
|
||||
|
@ -333,24 +322,20 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
Path(
|
||||
name = requireNotNull(raw.fileName) { "Invalid raw: No display name" },
|
||||
parent = requireNotNull(raw.directory) { "Invalid raw: No parent directory" })
|
||||
/**
|
||||
* The [MimeType] of the audio file. Only intended for display.
|
||||
*/
|
||||
|
||||
/** The [MimeType] of the audio file. Only intended for display. */
|
||||
val mimeType =
|
||||
MimeType(
|
||||
fromExtension = requireNotNull(raw.extensionMimeType) { "Invalid raw: No mime type" },
|
||||
fromFormat = raw.formatMimeType)
|
||||
/**
|
||||
* The size of the audio file, in bytes.
|
||||
*/
|
||||
|
||||
/** The size of the audio file, in bytes. */
|
||||
val size = requireNotNull(raw.size) { "Invalid raw: No size" }
|
||||
/**
|
||||
* The duration of the audio file, in milliseconds.
|
||||
*/
|
||||
|
||||
/** The duration of the audio file, in milliseconds. */
|
||||
val durationMs = requireNotNull(raw.durationMs) { "Invalid raw: No duration" }
|
||||
/**
|
||||
* The date the audio file was added to the device, as a unix epoch timestamp.
|
||||
*/
|
||||
|
||||
/** The date the audio file was added to the device, as a unix epoch timestamp. */
|
||||
val dateAdded = requireNotNull(raw.dateAdded) { "Invalid raw: No date added" }
|
||||
|
||||
private var _album: Album? = null
|
||||
|
@ -532,109 +517,57 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
|
|||
* ID is highly unstable and should only be used for accessing the audio file.
|
||||
*/
|
||||
var mediaStoreId: Long? = null,
|
||||
/**
|
||||
* @see Song.dateAdded
|
||||
*/
|
||||
/** @see Song.dateAdded */
|
||||
var dateAdded: Long? = null,
|
||||
/**
|
||||
* The latest date the [Song]'s audio file was modified, as a unix epoch timestamp.
|
||||
*/
|
||||
/** The latest date the [Song]'s audio file was modified, as a unix epoch timestamp. */
|
||||
var dateModified: Long? = null,
|
||||
/**
|
||||
* @see Song.path
|
||||
*/
|
||||
/** @see Song.path */
|
||||
var fileName: String? = null,
|
||||
/**
|
||||
* @see Song.path
|
||||
*/
|
||||
/** @see Song.path */
|
||||
var directory: Directory? = null,
|
||||
/**
|
||||
* @see Song.size
|
||||
*/
|
||||
/** @see Song.size */
|
||||
var size: Long? = null,
|
||||
/**
|
||||
* @see Song.durationMs
|
||||
*/
|
||||
/** @see Song.durationMs */
|
||||
var durationMs: Long? = null,
|
||||
/**
|
||||
* @see Song.mimeType
|
||||
*/
|
||||
/** @see Song.mimeType */
|
||||
var extensionMimeType: String? = null,
|
||||
/**
|
||||
* @see Song.mimeType
|
||||
*/
|
||||
/** @see Song.mimeType */
|
||||
var formatMimeType: String? = null,
|
||||
/**
|
||||
* @see Music.UID
|
||||
*/
|
||||
/** @see Music.UID */
|
||||
var musicBrainzId: String? = null,
|
||||
/**
|
||||
* @see Music.rawName
|
||||
*/
|
||||
/** @see Music.rawName */
|
||||
var name: String? = null,
|
||||
/**
|
||||
* @see Music.rawSortName
|
||||
*/
|
||||
/** @see Music.rawSortName */
|
||||
var sortName: String? = null,
|
||||
/**
|
||||
* @see Song.track
|
||||
*/
|
||||
/** @see Song.track */
|
||||
var track: Int? = null,
|
||||
/**
|
||||
* @see Song.disc
|
||||
*/
|
||||
/** @see Song.disc */
|
||||
var disc: Int? = null,
|
||||
/**
|
||||
* @see Song.date
|
||||
*/
|
||||
/** @see Song.date */
|
||||
var date: Date? = null,
|
||||
/**
|
||||
* @see Album.Raw.mediaStoreId
|
||||
*/
|
||||
/** @see Album.Raw.mediaStoreId */
|
||||
var albumMediaStoreId: Long? = null,
|
||||
/**
|
||||
* @see Album.Raw.musicBrainzId
|
||||
*/
|
||||
/** @see Album.Raw.musicBrainzId */
|
||||
var albumMusicBrainzId: String? = null,
|
||||
/**
|
||||
* @see Album.Raw.name
|
||||
*/
|
||||
/** @see Album.Raw.name */
|
||||
var albumName: String? = null,
|
||||
/**
|
||||
* @see Album.Raw.sortName
|
||||
*/
|
||||
/** @see Album.Raw.sortName */
|
||||
var albumSortName: String? = null,
|
||||
/**
|
||||
* @see Album.Raw.type
|
||||
*/
|
||||
/** @see Album.Raw.type */
|
||||
var albumTypes: List<String> = listOf(),
|
||||
/**
|
||||
* @see Artist.Raw.musicBrainzId
|
||||
*/
|
||||
/** @see Artist.Raw.musicBrainzId */
|
||||
var artistMusicBrainzIds: List<String> = listOf(),
|
||||
/**
|
||||
* @see Artist.Raw.name
|
||||
*/
|
||||
/** @see Artist.Raw.name */
|
||||
var artistNames: List<String> = listOf(),
|
||||
/**
|
||||
* @see Artist.Raw.sortName
|
||||
*/
|
||||
/** @see Artist.Raw.sortName */
|
||||
var artistSortNames: List<String> = listOf(),
|
||||
/**
|
||||
* @see Artist.Raw.musicBrainzId
|
||||
*/
|
||||
/** @see Artist.Raw.musicBrainzId */
|
||||
var albumArtistMusicBrainzIds: List<String> = listOf(),
|
||||
/**
|
||||
* @see Artist.Raw.name
|
||||
*/
|
||||
/** @see Artist.Raw.name */
|
||||
var albumArtistNames: List<String> = listOf(),
|
||||
/**
|
||||
* @see Artist.Raw.sortName
|
||||
*/
|
||||
/** @see Artist.Raw.sortName */
|
||||
var albumArtistSortNames: List<String> = listOf(),
|
||||
/**
|
||||
* @see Genre.Raw
|
||||
*/
|
||||
/** @see Genre.Raw.name */
|
||||
var genreNames: List<String> = listOf()
|
||||
)
|
||||
}
|
||||
|
@ -669,23 +602,24 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
|||
* TODO: Date ranges?
|
||||
*/
|
||||
val date: Date?
|
||||
|
||||
/**
|
||||
* The [Type] of this album, signifying the type of release it actually is.
|
||||
* Defaults to [Type.Album].
|
||||
*/
|
||||
|
||||
val type = raw.type ?: Type.Album(null)
|
||||
/**
|
||||
* The URI to a MediaStore-provided album cover. These images will be fast to load, but
|
||||
* at the cost of image quality.
|
||||
*/
|
||||
|
||||
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. */
|
||||
val durationMs: Long
|
||||
/**
|
||||
* The earliest date a song in this album was added, as a unix epoch timestamp.
|
||||
*/
|
||||
|
||||
/** The earliest date a song in this album was added, as a unix epoch timestamp. */
|
||||
val dateAdded: Long
|
||||
|
||||
init {
|
||||
|
@ -798,9 +732,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
|||
*/
|
||||
abstract val refinement: Refinement?
|
||||
|
||||
/**
|
||||
* The string resource corresponding to the name of this release type to show in the UI.
|
||||
*/
|
||||
/** The string resource corresponding to the name of this release type to show in the UI. */
|
||||
abstract val stringRes: Int
|
||||
|
||||
/**
|
||||
|
@ -999,34 +931,28 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
|||
* cover art.
|
||||
*/
|
||||
val mediaStoreId: Long,
|
||||
/**
|
||||
* @see Music.uid
|
||||
*/
|
||||
/** @see Music.uid */
|
||||
val musicBrainzId: UUID?,
|
||||
/**
|
||||
* @see Music.rawName
|
||||
*/
|
||||
/** @see Music.rawName */
|
||||
val name: String,
|
||||
/**
|
||||
* @see Music.rawSortName
|
||||
*/
|
||||
/** @see Music.rawSortName */
|
||||
val sortName: String?,
|
||||
/**
|
||||
* @see Album.type
|
||||
*/
|
||||
/** @see Album.type */
|
||||
val type: Type?,
|
||||
/**
|
||||
* @see Artist.Raw.name
|
||||
*/
|
||||
/** @see Artist.Raw.name */
|
||||
val rawArtists: List<Artist.Raw>
|
||||
) {
|
||||
// Albums are grouped as follows:
|
||||
// - If we have a MusicBrainz ID, only group by it. This allows different Albums with the
|
||||
// same name to be differentiated, which is common in large libraries.
|
||||
// - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase
|
||||
// artist name. This allows for case-insensitive artist/album grouping, which can be common
|
||||
// for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein").
|
||||
|
||||
// Cache the hash-code for HashMap efficiency.
|
||||
private val hashCode =
|
||||
musicBrainzId?.hashCode() ?: (31 * name.lowercase().hashCode() + rawArtists.hashCode())
|
||||
|
||||
// Make Album.Raw equality based on album name and raw artist lists in order to
|
||||
// differentiate between albums with the same name but different artists.
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
@ -1068,11 +994,13 @@ class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicP
|
|||
* thus included in this list.
|
||||
*/
|
||||
val albums: List<Album>
|
||||
|
||||
/**
|
||||
* The duration of all [Song]s in the artist, in milliseconds.
|
||||
* Will be null if there are no songs.
|
||||
*/
|
||||
val durationMs: Long?
|
||||
|
||||
/**
|
||||
* Whether this artist is considered a "collaborator", i.e it is not directly credited on
|
||||
* any [Album].
|
||||
|
@ -1159,19 +1087,19 @@ class Artist constructor(private val raw: Raw, songAlbums: List<Music>) : MusicP
|
|||
* **This is only meant for use within the music package.**
|
||||
*/
|
||||
class Raw(
|
||||
/**
|
||||
* @see Music.UID
|
||||
*/
|
||||
/** @see Music.UID */
|
||||
val musicBrainzId: UUID? = null,
|
||||
/**
|
||||
* @see Music.rawName
|
||||
*/
|
||||
/** @see Music.rawName */
|
||||
val name: String? = null,
|
||||
/**
|
||||
* @see Music.rawSortName
|
||||
*/
|
||||
/** @see Music.rawSortName */
|
||||
val sortName: String? = null
|
||||
) {
|
||||
// Artists are grouped as follows:
|
||||
// - If we have a MusicBrainz ID, only group by it. This allows different Artists with the
|
||||
// same name to be differentiated, which is common in large libraries.
|
||||
// - If we do not have a MusicBrainz ID, compare by the lowercase name. This allows artist
|
||||
// grouping to be case-insensitive.
|
||||
|
||||
// Cache the hashCode for HashMap efficiency.
|
||||
private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode()
|
||||
|
||||
|
@ -1271,6 +1199,10 @@ class Genre constructor(private val raw: Raw, override val songs: List<Song>) :
|
|||
*/
|
||||
val name: String? = null
|
||||
) {
|
||||
// Only group by the lowercase genre name. This allows Genre grouping to be
|
||||
// case-insensitive, which may be helpful in some libraries with different ways of
|
||||
// formatting genres.
|
||||
|
||||
// Cache the hashCode for HashMap efficiency.
|
||||
private val hashCode = name?.lowercase().hashCode()
|
||||
|
||||
|
@ -1361,22 +1293,16 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
|||
|
||||
private fun StringBuilder.appendDate(): StringBuilder {
|
||||
// Construct an ISO-8601 date, dropping precision that doesn't exist.
|
||||
append(year.toFixedString(4))
|
||||
append("-${(month ?: return this).toFixedString(2)}")
|
||||
append("-${(day ?: return this).toFixedString(2)}")
|
||||
append("T${(hour ?: return this).toFixedString(2)}")
|
||||
append(":${(minute ?: return this.append('Z')).toFixedString(2)}")
|
||||
append(":${(second ?: return this.append('Z')).toFixedString(2)}")
|
||||
append(year.toStringFixed(4))
|
||||
append("-${(month ?: return this).toStringFixed(2)}")
|
||||
append("-${(day ?: return this).toStringFixed(2)}")
|
||||
append("T${(hour ?: return this).toStringFixed(2)}")
|
||||
append(":${(minute ?: return this.append('Z')).toStringFixed(2)}")
|
||||
append(":${(second ?: return this.append('Z')).toStringFixed(2)}")
|
||||
return this.append('Z')
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an integer to a fixed-size [String] of the specified length.
|
||||
* @param len The end length of the formatted [String].
|
||||
* @return The integer as a formatted [String] prefixed with zeroes in order to make it
|
||||
* the specified length.
|
||||
*/
|
||||
private fun Int.toFixedString(len: Int) = toString().padStart(len, '0').substring(0 until len)
|
||||
private fun Int.toStringFixed(len: Int) = toString().padStart(len, '0').substring(0 until len)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
|
|
|
@ -24,24 +24,13 @@ import org.oxycblt.auxio.IntegerTable
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
enum class MusicMode {
|
||||
/**
|
||||
* Configure with respect to [Song] instances.
|
||||
*/
|
||||
/** Configure with respect to [Song] instances. */
|
||||
SONGS,
|
||||
|
||||
/**
|
||||
* Configure with respect to [Album] instances.
|
||||
*/
|
||||
/** Configure with respect to [Album] instances. */
|
||||
ALBUMS,
|
||||
|
||||
/**
|
||||
* Configure with respect to [Artist] instances.
|
||||
*/
|
||||
/** Configure with respect to [Artist] instances. */
|
||||
ARTISTS,
|
||||
|
||||
/**
|
||||
* Configure with respect to [Genre] instances.
|
||||
*/
|
||||
/** Configure with respect to [Genre] instances. */
|
||||
GENRES;
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,8 +20,6 @@ package org.oxycblt.auxio.music
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import org.oxycblt.auxio.music.MusicStore.Callback
|
||||
import org.oxycblt.auxio.music.MusicStore.Library
|
||||
import org.oxycblt.auxio.music.storage.useQuery
|
||||
import org.oxycblt.auxio.music.storage.contentResolverSafe
|
||||
|
||||
|
@ -92,8 +90,8 @@ class MusicStore private constructor() {
|
|||
|
||||
init {
|
||||
// The data passed to Library initially are complete, but are still volitaile.
|
||||
// Finalize them to ensure they are well-formed. Initialize the UID map in the
|
||||
// same loop for efficiency.
|
||||
// Finalize them to ensure they are well-formed. Also initialize the UID map in
|
||||
// the same loop for efficiency.
|
||||
for (song in songs) {
|
||||
song._finalize()
|
||||
uidMap[song.uid] = song
|
||||
|
@ -146,7 +144,7 @@ class MusicStore private constructor() {
|
|||
|
||||
/**
|
||||
* Convert a [Genre] from an another library into a [Genre] in this [Library].
|
||||
* @param song The [Genre] to convert.
|
||||
* @param genre The [Genre] to convert.
|
||||
* @return The analogous [Genre] in this [Library], or null if it does not exist.
|
||||
*/
|
||||
fun sanitize(genre: Genre) = find<Genre>(genre.uid)
|
||||
|
@ -170,9 +168,7 @@ class MusicStore private constructor() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback for changes in the music library.
|
||||
*/
|
||||
/** A callback for changes in the music library. */
|
||||
interface Callback {
|
||||
/**
|
||||
* Called when the current [Library] has changed.
|
||||
|
|
|
@ -31,17 +31,11 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
|
|||
private val indexer = Indexer.getInstance()
|
||||
|
||||
private val _indexerState = MutableStateFlow<Indexer.State?>(null)
|
||||
/**
|
||||
* The current music loading state, or null if no loading is going on.
|
||||
* @see Indexer.State
|
||||
*/
|
||||
/** The current music loading state, or null if no loading is going on. */
|
||||
val indexerState: StateFlow<Indexer.State?> = _indexerState
|
||||
|
||||
private val _statistics = MutableStateFlow<Statistics?>(null)
|
||||
/**
|
||||
* Statistics about the last completed music load.
|
||||
* @see Statistics
|
||||
*/
|
||||
/** [Statistics] about the last completed music load. */
|
||||
val statistics: StateFlow<Statistics?>
|
||||
get() = _statistics
|
||||
|
||||
|
@ -68,16 +62,12 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests that the music library should be re-loaded while leveraging the cache.
|
||||
*/
|
||||
/** Requests that the music library should be re-loaded while leveraging the cache. */
|
||||
fun refresh() {
|
||||
indexer.requestReindex(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests that the music library should be re-loaded while ignoring the cache.
|
||||
*/
|
||||
/** Requests that the music library be re-loaded without the cache. */
|
||||
fun rescan() {
|
||||
indexer.requestReindex(false)
|
||||
}
|
||||
|
|
|
@ -129,19 +129,14 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
*/
|
||||
val intCode: Int
|
||||
// Sort's integer representation is formatted as AMMMM, where A is a bitflag
|
||||
// representing on if the mode is ascending or descending, and M is the integer
|
||||
// representation of the sort mode.
|
||||
// representing if the sort is in ascending or descending order, and M is the
|
||||
// integer representation of the sort mode.
|
||||
get() = mode.intCode.shl(1) or if (isAscending) 1 else 0
|
||||
|
||||
sealed class Mode {
|
||||
/**
|
||||
* The integer representation of this sort mode.
|
||||
*/
|
||||
/** The integer representation of this sort mode. */
|
||||
abstract val intCode: Int
|
||||
|
||||
/**
|
||||
* The item ID of this sort mode in menu resources.
|
||||
*/
|
||||
/** The item ID of this sort mode in menu resources. */
|
||||
abstract val itemId: Int
|
||||
|
||||
/**
|
||||
|
@ -276,9 +271,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
compareBy(BasicComparator.ALBUM))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by the duration of an item.
|
||||
*/
|
||||
/** Sort by the duration of an item. */
|
||||
object ByDuration : Mode() {
|
||||
override val intCode: Int
|
||||
get() = IntegerTable.SORT_BY_DURATION
|
||||
|
@ -494,9 +487,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* A shared instance configured for [Artist]s that can be re-used.
|
||||
*/
|
||||
/** A re-usable configured for [Artist]s.. */
|
||||
val ARTISTS: Comparator<List<Artist>> = ListComparator(BasicComparator.ARTIST)
|
||||
}
|
||||
}
|
||||
|
@ -520,21 +511,13 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* A shared instance configured for [Song]s that can be re-used.
|
||||
*/
|
||||
/** A re-usable instance configured for [Song]s. */
|
||||
val SONG: Comparator<Song> = BasicComparator()
|
||||
/**
|
||||
* A shared instance configured for [Album]s that can be re-used.
|
||||
*/
|
||||
/** A re-usable instance configured for [Album]s. */
|
||||
val ALBUM: Comparator<Album> = BasicComparator()
|
||||
/**
|
||||
* A shared instance configured for [Artist]s that can be re-used.
|
||||
*/
|
||||
/** A re-usable instance configured for [Artist]s. */
|
||||
val ARTIST: Comparator<Artist> = BasicComparator()
|
||||
/**
|
||||
* A shared instance configured for [Genre]s that can be re-used.
|
||||
*/
|
||||
/** A re-usable instance configured for [Genre]s. */
|
||||
val GENRE: Comparator<Genre> = BasicComparator()
|
||||
}
|
||||
}
|
||||
|
@ -553,17 +536,11 @@ data class Sort(val mode: Mode, val isAscending: Boolean) {
|
|||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* A shared instance configured for [Int]s that can be re-used.
|
||||
*/
|
||||
/** A re-usable instance configured for [Int]s. */
|
||||
val INT = NullableComparator<Int>()
|
||||
/**
|
||||
* A shared instance configured for [Long]s that can be re-used.
|
||||
*/
|
||||
/** A re-usable instance configured for [Long]s. */
|
||||
val LONG = NullableComparator<Long>()
|
||||
/**
|
||||
* A shared instance configured for [Date]s that can be re-used.
|
||||
*/
|
||||
/** A re-usable instance configured for [Date]s. */
|
||||
val DATE = NullableComparator<Date>()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ interface CacheExtractor {
|
|||
fun init()
|
||||
|
||||
/**
|
||||
* Finalize the Extractor by writing the newly-loaded [Song.Raw] back into the cache,
|
||||
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache,
|
||||
* alongside freeing up memory.
|
||||
* @param rawSongs The songs to write into the cache.
|
||||
*/
|
||||
|
@ -437,123 +437,58 @@ private class CacheDatabase(context: Context) :
|
|||
* Defines the columns used in this database.
|
||||
*/
|
||||
private object Columns {
|
||||
/**
|
||||
* @see Song.Raw.mediaStoreId
|
||||
*/
|
||||
/** @see Song.Raw.mediaStoreId */
|
||||
const val MEDIA_STORE_ID = "msid"
|
||||
/**
|
||||
* @see Song.Raw.dateAdded
|
||||
*/
|
||||
/** @see Song.Raw.dateAdded */
|
||||
const val DATE_ADDED = "date_added"
|
||||
/**
|
||||
* @see Song.Raw.dateModified
|
||||
*/
|
||||
/** @see Song.Raw.dateModified */
|
||||
const val DATE_MODIFIED = "date_modified"
|
||||
|
||||
/**
|
||||
* @see Song.Raw.size
|
||||
*/
|
||||
/** @see Song.Raw.size */
|
||||
const val SIZE = "size"
|
||||
/**
|
||||
* @see Song.Raw.durationMs
|
||||
*/
|
||||
/** @see Song.Raw.durationMs */
|
||||
const val DURATION = "duration"
|
||||
/**
|
||||
* @see Song.Raw.formatMimeType
|
||||
*/
|
||||
/** @see Song.Raw.formatMimeType */
|
||||
const val FORMAT_MIME_TYPE = "fmt_mime"
|
||||
|
||||
/**
|
||||
* @see Song.Raw.musicBrainzId
|
||||
*/
|
||||
/** @see Song.Raw.musicBrainzId */
|
||||
const val MUSIC_BRAINZ_ID = "mbid"
|
||||
/**
|
||||
* @see Song.Raw.name
|
||||
*/
|
||||
/** @see Song.Raw.name */
|
||||
const val NAME = "name"
|
||||
/**
|
||||
* @see Song.Raw.sortName
|
||||
*/
|
||||
/** @see Song.Raw.sortName */
|
||||
const val SORT_NAME = "sort_name"
|
||||
|
||||
/**
|
||||
* @see Song.Raw.track
|
||||
*/
|
||||
/** @see Song.Raw.track */
|
||||
const val TRACK = "track"
|
||||
/**
|
||||
* @see Song.Raw.disc
|
||||
*/
|
||||
/** @see Song.Raw.disc */
|
||||
const val DISC = "disc"
|
||||
/**
|
||||
* @see [Song.Raw.date
|
||||
*/
|
||||
/** @see Song.Raw.date */
|
||||
const val DATE = "date"
|
||||
|
||||
/**
|
||||
* @see [Song.Raw.albumMusicBrainzId
|
||||
*/
|
||||
/** @see Song.Raw.albumMusicBrainzId */
|
||||
const val ALBUM_MUSIC_BRAINZ_ID = "album_mbid"
|
||||
/**
|
||||
* @see Song.Raw.albumName
|
||||
*/
|
||||
/** @see Song.Raw.albumName */
|
||||
const val ALBUM_NAME = "album"
|
||||
/**
|
||||
* @see Song.Raw.albumSortName
|
||||
*/
|
||||
/** @see Song.Raw.albumSortName */
|
||||
const val ALBUM_SORT_NAME = "album_sort"
|
||||
/**
|
||||
* @see Song.Raw.albumReleaseTypes
|
||||
*/
|
||||
/** @see Song.Raw.albumTypes */
|
||||
const val ALBUM_TYPES = "album_types"
|
||||
|
||||
/**
|
||||
* @see Song.Raw.artistMusicBrainzIds
|
||||
*/
|
||||
/** @see Song.Raw.artistMusicBrainzIds */
|
||||
const val ARTIST_MUSIC_BRAINZ_IDS = "artists_mbid"
|
||||
/**
|
||||
* @see Song.Raw.artistNames
|
||||
*/
|
||||
/** @see Song.Raw.artistNames */
|
||||
const val ARTIST_NAMES = "artists"
|
||||
/**
|
||||
* @see Song.Raw.artistSortNames
|
||||
*/
|
||||
/** @see Song.Raw.artistSortNames */
|
||||
const val ARTIST_SORT_NAMES = "artists_sort"
|
||||
|
||||
/**
|
||||
* @see Song.Raw.albumArtistMusicBrainzIds
|
||||
*/
|
||||
/** @see Song.Raw.albumArtistMusicBrainzIds */
|
||||
const val ALBUM_ARTIST_MUSIC_BRAINZ_IDS = "album_artists_mbid"
|
||||
/**
|
||||
* @see Song.Raw.albumArtistNames
|
||||
*/
|
||||
/** @see Song.Raw.albumArtistNames */
|
||||
const val ALBUM_ARTIST_NAMES = "album_artists"
|
||||
/**
|
||||
* @see Song.Raw.albumArtistSortNames
|
||||
*/
|
||||
/** @see Song.Raw.albumArtistSortNames */
|
||||
const val ALBUM_ARTIST_SORT_NAMES = "album_artists_sort"
|
||||
|
||||
/**
|
||||
* @see Song.Raw.genreNames
|
||||
*/
|
||||
/** @see Song.Raw.genreNames */
|
||||
const val GENRE_NAMES = "genres"
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The file name of the database.
|
||||
*/
|
||||
const val DB_NAME = "auxio_music_cache.db"
|
||||
|
||||
/**
|
||||
* The current version of the database. Increment whenever a breaking change is made
|
||||
* to the schema. When incremented, the database will be wiped.
|
||||
*/
|
||||
const val DB_VERSION = 1
|
||||
|
||||
/**
|
||||
* The table containing the cached [Song.Raw] instances.
|
||||
*/
|
||||
const val TABLE_RAW_SONGS = "raw_songs"
|
||||
private const val DB_NAME = "auxio_music_cache.db"
|
||||
private const val DB_VERSION = 1
|
||||
private const val TABLE_RAW_SONGS = "raw_songs"
|
||||
|
||||
@Volatile private var INSTANCE: CacheDatabase? = null
|
||||
|
||||
|
|
|
@ -81,14 +81,10 @@ abstract class MediaStoreExtractor(
|
|||
* @return A [Cursor] of the music data returned from the database.
|
||||
*/
|
||||
open fun init(): Cursor {
|
||||
// Initialize sub-extractors for later use.
|
||||
cacheExtractor.init()
|
||||
|
||||
val start = System.currentTimeMillis()
|
||||
cacheExtractor.init()
|
||||
val settings = Settings(context)
|
||||
val storageManager = context.getSystemServiceCompat(StorageManager::class)
|
||||
// Set up the volume list for concrete implementations to use.
|
||||
volumes = storageManager.storageVolumesCompat
|
||||
|
||||
val args = mutableListOf<String>()
|
||||
var selector = BASE_SELECTOR
|
||||
|
@ -151,8 +147,6 @@ abstract class MediaStoreExtractor(
|
|||
artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST)
|
||||
albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST)
|
||||
|
||||
logD("Assembling genre map")
|
||||
|
||||
// Since we can't obtain the genre tag from a song query, we must construct our own
|
||||
// equivalent from genre database queries. Theoretically, this isn't needed since
|
||||
// MetadataLayer will fill this in for us, but I'd imagine there are some obscure
|
||||
|
@ -183,18 +177,21 @@ abstract class MediaStoreExtractor(
|
|||
}
|
||||
}
|
||||
|
||||
volumes = storageManager.storageVolumesCompat
|
||||
logD("Finished initialization in ${System.currentTimeMillis() - start}ms")
|
||||
|
||||
return cursor
|
||||
}
|
||||
|
||||
/** Finalize this instance by closing the cursor and finalizing the cache. */
|
||||
/**
|
||||
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache,
|
||||
* alongside freeing up memory.
|
||||
* @param rawSongs The songs to write into the cache.
|
||||
*/
|
||||
fun finalize(rawSongs: List<Song.Raw>) {
|
||||
// Free the cursor (and it's resources)
|
||||
cursor?.close()
|
||||
cursor = null
|
||||
|
||||
// Finalize sub-extractors
|
||||
cacheExtractor.finalize(rawSongs)
|
||||
}
|
||||
|
||||
|
@ -502,6 +499,7 @@ open class Api29MediaStoreExtractor(context: Context, cacheExtractor: CacheExtra
|
|||
|
||||
override fun init(): Cursor {
|
||||
val cursor = super.init()
|
||||
// Set up cursor indices for later use.
|
||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||
return cursor
|
||||
}
|
||||
|
@ -537,6 +535,7 @@ class Api30MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor)
|
|||
|
||||
override fun init(): Cursor {
|
||||
val cursor = super.init()
|
||||
// Set up cursor indices for later use.
|
||||
trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
|
||||
discIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
||||
return cursor
|
||||
|
|
|
@ -56,8 +56,9 @@ class MetadataExtractor(
|
|||
fun init() = mediaStoreExtractor.init().count
|
||||
|
||||
/**
|
||||
* Finalize this extractor with the newly parsed [Song.Raw]. This actually finalizes the
|
||||
* sub-extractors that this instance relies on.
|
||||
* Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache,
|
||||
* alongside freeing up memory.
|
||||
* @param rawSongs The songs to write into the cache.
|
||||
*/
|
||||
fun finalize(rawSongs: List<Song.Raw>) = mediaStoreExtractor.finalize(rawSongs)
|
||||
|
||||
|
@ -85,7 +86,6 @@ class MetadataExtractor(
|
|||
spin@ while (true) {
|
||||
for (i in taskPool.indices) {
|
||||
val task = taskPool[i]
|
||||
|
||||
if (task != null) {
|
||||
val finishedRaw = task.get()
|
||||
if (finishedRaw != null) {
|
||||
|
@ -105,7 +105,6 @@ class MetadataExtractor(
|
|||
// Spin until all of the remaining tasks are complete.
|
||||
for (i in taskPool.indices) {
|
||||
val task = taskPool[i]
|
||||
|
||||
if (task != null) {
|
||||
val finishedRaw = task.get() ?: continue@spin
|
||||
emit(finishedRaw)
|
||||
|
@ -118,9 +117,6 @@ class MetadataExtractor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The amount of [Task]s this instance can return
|
||||
*/
|
||||
private const val TASK_CAPACITY = 8
|
||||
}
|
||||
}
|
||||
|
@ -158,7 +154,6 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
logW(e.stackTraceToString())
|
||||
null
|
||||
}
|
||||
|
||||
if (format == null) {
|
||||
logD("Nothing could be extracted for ${raw.name}")
|
||||
return raw
|
||||
|
|
|
@ -110,7 +110,7 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList<String>
|
|||
}
|
||||
|
||||
if (currentString.isNotEmpty()) {
|
||||
// Had an in-progress split string we should add.
|
||||
// Had an in-progress split string that is now terminated, add it..
|
||||
split.add(currentString.trim())
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
|
||||
/**
|
||||
* An adapter responsible for showing a list of [Artist] choices in [ArtistPickerDialog].
|
||||
* @param listener A [BasicListListener] for list interactions.
|
||||
* @param listener A [BasicListListener] to bind interactions to.
|
||||
* @author OxygenCobalt.
|
||||
*/
|
||||
class ArtistChoiceAdapter(private val listener: BasicListListener) :
|
||||
|
@ -45,8 +45,8 @@ class ArtistChoiceAdapter(private val listener: BasicListListener) :
|
|||
holder.bind(artists[position], listener)
|
||||
|
||||
/**
|
||||
* Immediately update the tab array. This should be used when initializing the list.
|
||||
* @param newTabs The new array of tabs to show.
|
||||
* Immediately update the [Artist] choices.
|
||||
* @param newArtists The new [Artist]s to show.
|
||||
*/
|
||||
fun submitList(newArtists: List<Artist>) {
|
||||
if (newArtists != artists) {
|
||||
|
@ -58,7 +58,7 @@ class ArtistChoiceAdapter(private val listener: BasicListListener) :
|
|||
|
||||
/**
|
||||
* A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical
|
||||
* [Artist] item, for use with [ArtistChoiceAdapter]. Use [new] to instantiate a new instance.
|
||||
* [Artist] item, for use with [ArtistChoiceAdapter]. Use [new] to create an instance.
|
||||
*/
|
||||
class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
|
||||
DialogRecyclerView.ViewHolder(binding.root) {
|
||||
|
|
|
@ -38,7 +38,7 @@ import org.oxycblt.auxio.util.collectImmediately
|
|||
*/
|
||||
abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>(), BasicListListener {
|
||||
protected val pickerModel: PickerViewModel by viewModels()
|
||||
// Okay to leak this since the Listener will not be called until after full initialization.
|
||||
// Okay to leak this since the Listener will not be called until after initialization.
|
||||
private val artistAdapter = ArtistChoiceAdapter(@Suppress("LeakingThis") this)
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
|
@ -53,7 +53,7 @@ abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerB
|
|||
|
||||
collectImmediately(pickerModel.currentArtists) { artists ->
|
||||
if (!artists.isNullOrEmpty()) {
|
||||
// Make sure the artist choices align with the current music library.
|
||||
// Make sure the artist choices align with any changes in the music library.
|
||||
// TODO: I really don't think it makes sense to do this. I'd imagine it would
|
||||
// be more productive to just exit this dialog rather than try to update it.
|
||||
artistAdapter.submitList(artists)
|
||||
|
|
|
@ -27,7 +27,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
|
||||
/**
|
||||
* [RecyclerView.Adapter] that manages a list of [Directory] instances.
|
||||
* @param listener [Listener] for list interactions.
|
||||
* @param listener A [DirectoryAdapter.Listener] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class DirectoryAdapter(private val listener: Listener) : RecyclerView.Adapter<MusicDirViewHolder>() {
|
||||
|
@ -78,23 +78,34 @@ class DirectoryAdapter(private val listener: Listener) : RecyclerView.Adapter<Mu
|
|||
notifyItemRemoved(idx)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Listener for [DirectoryAdapter] interactions.
|
||||
*/
|
||||
/** A Listener for [DirectoryAdapter] interactions. */
|
||||
interface Listener {
|
||||
fun onRemoveDirectory(dir: Directory)
|
||||
}
|
||||
}
|
||||
|
||||
/** The viewholder for [DirectoryAdapter]. Not intended for use in other adapters. */
|
||||
/**
|
||||
* A [RecyclerView.Recycler] that displays a [Directory]. Use [new] to create an instance.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBinding) :
|
||||
DialogRecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: Directory, listener: DirectoryAdapter.Listener) {
|
||||
binding.dirPath.text = item.resolveName(binding.context)
|
||||
binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(item) }
|
||||
/**
|
||||
* Bind new data to this instance.
|
||||
* @param dir The new [Directory] to bind.
|
||||
* @param listener A [DirectoryAdapter.Listener] to bind interactions to.
|
||||
*/
|
||||
fun bind(dir: Directory, listener: DirectoryAdapter.Listener) {
|
||||
binding.dirPath.text = dir.resolveName(binding.context)
|
||||
binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(dir) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create a new instance.
|
||||
* @param parent The parent to inflate this instance from.
|
||||
* @return A new instance.
|
||||
*/
|
||||
fun new(parent: View) =
|
||||
MusicDirViewHolder(ItemMusicDirBinding.inflate(parent.context.inflater))
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* 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
|
|
@ -59,8 +59,7 @@ class MusicDirsDialog :
|
|||
.setPositiveButton(R.string.lbl_save) { _, _ ->
|
||||
val dirs = settings.getMusicDirs(storageManager)
|
||||
val newDirs =
|
||||
MusicDirectories(
|
||||
dirs = dirAdapter.dirs, shouldInclude = isUiModeInclude(requireBinding()))
|
||||
MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
|
||||
if (dirs != newDirs) {
|
||||
logD("Committing changes")
|
||||
settings.setMusicDirs(newDirs)
|
||||
|
@ -70,7 +69,7 @@ class MusicDirsDialog :
|
|||
|
||||
override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) {
|
||||
val launcher =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath)
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs)
|
||||
|
||||
// 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
|
||||
|
@ -78,7 +77,6 @@ class MusicDirsDialog :
|
|||
// and the app from crashing in the latter.
|
||||
requireDialog().setOnShowListener {
|
||||
val dialog = it as AlertDialog
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener {
|
||||
logD("Opening launcher")
|
||||
launcher.launch(null)
|
||||
|
@ -94,7 +92,6 @@ class MusicDirsDialog :
|
|||
|
||||
if (savedInstanceState != null) {
|
||||
val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS)
|
||||
|
||||
if (pendingDirs != null) {
|
||||
dirs =
|
||||
MusicDirectories(
|
||||
|
@ -136,14 +133,26 @@ class MusicDirsDialog :
|
|||
requireBinding().dirsEmpty.isVisible = dirAdapter.dirs.isEmpty()
|
||||
}
|
||||
|
||||
private fun addDocTreePath(uri: Uri?) {
|
||||
/**
|
||||
* Add a Document Tree [Uri] chosen by the user to the current [MusicDirectories] instance.
|
||||
* @param uri The document tree [Uri] to add, chosen by the user. Will do nothing if the [Uri]
|
||||
* is null or not valid.
|
||||
*/
|
||||
private fun addDocumentTreeUriToDirs(uri: Uri?) {
|
||||
if (uri == null) {
|
||||
// A null URI means that the user left the file picker without picking a directory
|
||||
logD("No URI given (user closed the dialog)")
|
||||
return
|
||||
}
|
||||
|
||||
val dir = parseExcludedUri(uri)
|
||||
// Convert the document tree URI into it's relative path form, which can then be
|
||||
// parsed into a Directory instance.
|
||||
val docUri =
|
||||
DocumentsContract.buildDocumentUriUsingTree(
|
||||
uri, DocumentsContract.getTreeDocumentId(uri))
|
||||
val treeUri = DocumentsContract.getTreeDocumentId(docUri)
|
||||
val dir = Directory.fromDocumentTreeUri(storageManager, treeUri)
|
||||
|
||||
if (dir != null) {
|
||||
dirAdapter.add(dir)
|
||||
requireBinding().dirsEmpty.isVisible = false
|
||||
|
@ -152,19 +161,6 @@ class MusicDirsDialog :
|
|||
}
|
||||
}
|
||||
|
||||
private fun parseExcludedUri(uri: Uri): Directory? {
|
||||
// Turn the raw URI into a document tree URI
|
||||
val docUri =
|
||||
DocumentsContract.buildDocumentUriUsingTree(
|
||||
uri, DocumentsContract.getTreeDocumentId(uri))
|
||||
|
||||
// Turn it into a semi-usable path
|
||||
val treeUri = DocumentsContract.getTreeDocumentId(docUri)
|
||||
|
||||
// Parsing handles the rest
|
||||
return Directory.fromDocumentTreeUri(storageManager, treeUri)
|
||||
}
|
||||
|
||||
private fun updateMode() {
|
||||
val binding = requireBinding()
|
||||
if (isUiModeInclude(binding)) {
|
||||
|
@ -174,6 +170,9 @@ class MusicDirsDialog :
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if the UI has currently configured [MusicDirectories.shouldInclude] to be true.
|
||||
*/
|
||||
private fun isUiModeInclude(binding: DialogMusicDirsBinding) =
|
||||
binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include
|
||||
|
||||
|
|
|
@ -48,9 +48,8 @@ val Context.contentResolverSafe: ContentResolver
|
|||
* arguments should be filled in are represented with a "?".
|
||||
* @param args The arguments used for the selector.
|
||||
* @return A [Cursor] of the queried values, organized by the column projection.
|
||||
* @throws IllegalStateException If the [ContentResolver] did not successfully return
|
||||
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
|
||||
* @see ContentResolver.query
|
||||
* a queried [Cursor].
|
||||
*/
|
||||
fun ContentResolver.safeQuery(
|
||||
uri: Uri,
|
||||
|
@ -71,9 +70,8 @@ fun ContentResolver.safeQuery(
|
|||
* @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
|
||||
* [Cursor] is empty.
|
||||
* @throws IllegalStateException If the [ContentResolver] did not successfully return
|
||||
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
|
||||
* @see ContentResolver.query
|
||||
* a queried [Cursor].
|
||||
*/
|
||||
inline fun <reified R> ContentResolver.useQuery(
|
||||
uri: Uri,
|
||||
|
|
|
@ -56,9 +56,7 @@ class Indexer private constructor() {
|
|||
private var controller: Controller? = null
|
||||
private var callback: Callback? = null
|
||||
|
||||
/**
|
||||
* Whether this instance is currently loading music.
|
||||
*/
|
||||
/** Whether music loading is occurring or not. */
|
||||
val isIndexing: Boolean
|
||||
get() = indexingState != null
|
||||
|
||||
|
@ -226,6 +224,7 @@ class Indexer private constructor() {
|
|||
} else {
|
||||
WriteOnlyCacheExtractor(context)
|
||||
}
|
||||
|
||||
val mediaStoreExtractor =
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
|
||||
|
@ -234,7 +233,9 @@ class Indexer private constructor() {
|
|||
Api29MediaStoreExtractor(context, cacheDatabase)
|
||||
else -> Api21MediaStoreExtractor(context, cacheDatabase)
|
||||
}
|
||||
|
||||
val metadataExtractor = MetadataExtractor(context, mediaStoreExtractor)
|
||||
|
||||
val songs = buildSongs(metadataExtractor, Settings(context))
|
||||
if (songs.isEmpty()) {
|
||||
// No songs, nothing else to do.
|
||||
|
@ -248,6 +249,7 @@ class Indexer private constructor() {
|
|||
val artists = buildArtists(songs, albums)
|
||||
val genres = buildGenres(songs)
|
||||
logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
|
||||
|
||||
return MusicStore.Library(songs, albums, artists, genres)
|
||||
}
|
||||
|
||||
|
@ -265,11 +267,10 @@ class Indexer private constructor() {
|
|||
): List<Song> {
|
||||
logD("Starting indexing process")
|
||||
val start = System.currentTimeMillis()
|
||||
// Start initializing the extractors. Here, we will signal that we are loading music,
|
||||
// but have no ETA on how far we are.
|
||||
// Start initializing the extractors. Use an indeterminate state, as there is no ETA on
|
||||
// how long a media database query will take.
|
||||
emitIndexing(Indexing.Indeterminate)
|
||||
val total = metadataExtractor.init()
|
||||
// Handle if we were canceled while initializing the extractors.
|
||||
yield()
|
||||
|
||||
// Note: We use a set here so we can eliminate song duplicates.
|
||||
|
@ -278,19 +279,20 @@ class Indexer private constructor() {
|
|||
metadataExtractor.parse { rawSong ->
|
||||
songs.add(Song(rawSong, settings))
|
||||
rawSongs.add(rawSong)
|
||||
// Handle if we were cancelled while loading a song.
|
||||
yield()
|
||||
|
||||
// Now we can signal a defined progress by showing how many songs we have
|
||||
// loaded, and the projected amount of songs we found in the library
|
||||
// (obtained by the extractors)
|
||||
yield()
|
||||
emitIndexing(Indexing.Songs(songs.size, total))
|
||||
}
|
||||
|
||||
// Finalize the extractors with the songs we have no loaded. There is no ETA
|
||||
// Finalize the extractors with the songs we have now loaded. There is no ETA
|
||||
// on this process, so go back to an indeterminate state.
|
||||
emitIndexing(Indexing.Indeterminate)
|
||||
metadataExtractor.finalize(rawSongs)
|
||||
logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
|
||||
|
||||
// Ensure that sorting order is consistent so that grouping is also consistent.
|
||||
// Rolling this into the set is not an option, as songs with the same sort result
|
||||
// would be lost.
|
||||
|
@ -330,6 +332,7 @@ class Indexer private constructor() {
|
|||
// Add every raw artist credited to each Song/Album to the grouping. This way,
|
||||
// different multi-artist combinations are not treated as different artists.
|
||||
val musicByArtist = mutableMapOf<Artist.Raw, MutableList<Music>>()
|
||||
|
||||
for (song in songs) {
|
||||
for (rawArtist in song._rawArtists) {
|
||||
musicByArtist.getOrPut(rawArtist) { mutableListOf() }.add(song)
|
||||
|
@ -396,8 +399,6 @@ class Indexer private constructor() {
|
|||
* process.
|
||||
*/
|
||||
private suspend fun emitCompletion(response: Response) {
|
||||
// Handle if this co-routine was canceled in the period between the last loading state
|
||||
// and this completion state.
|
||||
yield()
|
||||
// Swap to the Main thread so that downstream callbacks don't crash from being on
|
||||
// a background thread. Does not occur in emitIndexing due to efficiency reasons.
|
||||
|
@ -415,9 +416,7 @@ class Indexer private constructor() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the current state of the music loading process.
|
||||
*/
|
||||
/** Represents the current state of [Indexer]. */
|
||||
sealed class State {
|
||||
/**
|
||||
* Music loading is ongoing.
|
||||
|
@ -435,7 +434,7 @@ class Indexer private constructor() {
|
|||
}
|
||||
|
||||
/**
|
||||
* The current progress of the music loader. Usually encapsulated in a [State].
|
||||
* Represents the current progress of the music loader. Usually encapsulated in a [State].
|
||||
* @see State.Indexing
|
||||
*/
|
||||
sealed class Indexing {
|
||||
|
@ -453,9 +452,7 @@ class Indexer private constructor() {
|
|||
class Songs(val current: Int, val total: Int) : Indexing()
|
||||
}
|
||||
|
||||
/**
|
||||
* The possible outcomes of the music loading process.
|
||||
*/
|
||||
/** Represents the possible outcomes of the music loading process. */
|
||||
sealed class Response {
|
||||
/**
|
||||
* Music load was successful and produced a [MusicStore.Library].
|
||||
|
@ -469,14 +466,10 @@ class Indexer private constructor() {
|
|||
*/
|
||||
data class Err(val throwable: Throwable) : Response()
|
||||
|
||||
/**
|
||||
* Music loading occurred, but resulted in no music.
|
||||
*/
|
||||
/** Music loading occurred, but resulted in no music. */
|
||||
object NoMusic : Response()
|
||||
|
||||
/**
|
||||
* Music loading could not occur due to a lack of storage permissions.
|
||||
*/
|
||||
/** Music loading could not occur due to a lack of storage permissions. */
|
||||
object NoPerms : Response()
|
||||
}
|
||||
|
||||
|
|
|
@ -109,9 +109,7 @@ class ObservingNotification(context: Context) : ServiceNotification(context, IND
|
|||
get() = IntegerTable.INDEXER_NOTIFICATION_CODE
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared channel that [IndexingNotification] and [ObservingNotification] post to.
|
||||
*/
|
||||
/** Notification channel shared by [IndexingNotification] and [ObservingNotification]. */
|
||||
private val INDEXER_CHANNEL =
|
||||
ServiceNotification.ChannelInfo(
|
||||
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER", nameRes = R.string.lbl_indexer)
|
||||
|
|
|
@ -164,11 +164,16 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
|
||||
// --- INTERNAL ---
|
||||
|
||||
/**
|
||||
* Update the current state to "Active", in which the service signals that music
|
||||
* loading is on-going.
|
||||
* @param state The current music loading state.
|
||||
*/
|
||||
private fun updateActiveSession(state: Indexer.Indexing) {
|
||||
// When loading, we want to enter the foreground state so that android does
|
||||
// not shut off the loading process. Note that while we will always post the
|
||||
// notification when initially starting, we will not update the notification
|
||||
// unless it indicates that we have changed it.
|
||||
// unless it indicates that it has changed.
|
||||
val changed = indexingNotification.updateIndexingState(state)
|
||||
if (!foregroundManager.tryStartForeground(indexingNotification) && changed) {
|
||||
logD("Notification changed, re-posting notification")
|
||||
|
@ -178,6 +183,10 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
wakeLock.acquireSafe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current state to "Idle", in which it either does nothing or signals
|
||||
* that it's currently monitoring the music library for changes.
|
||||
*/
|
||||
private fun updateIdleSession() {
|
||||
if (settings.shouldBeObserving) {
|
||||
// There are a few reasons why we stay in the foreground with automatic rescanning:
|
||||
|
@ -199,6 +208,9 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
wakeLock.releaseSafe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to safely acquire a [PowerManager.WakeLock] without crashes/inefficiency.
|
||||
*/
|
||||
private fun PowerManager.WakeLock.acquireSafe() {
|
||||
// Avoid unnecessary acquire calls.
|
||||
if (!wakeLock.isHeld) {
|
||||
|
@ -210,6 +222,9 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to safely release a [PowerManager.WakeLock] without crashes/inefficiency.
|
||||
*/
|
||||
private fun PowerManager.WakeLock.releaseSafe() {
|
||||
// Avoid unnecessary release calls.
|
||||
if (wakeLock.isHeld) {
|
||||
|
@ -277,16 +292,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
|||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The amount of time to hold the wake lock when loading music, in milliseconds.
|
||||
* Equivalent to one minute.
|
||||
*/
|
||||
private const val WAKELOCK_TIMEOUT_MS = 60 * 1000L
|
||||
|
||||
/**
|
||||
* The amount of time to wait between a change in the music library and to start
|
||||
* the music loading process, in milliseconds. Equivalent to half a second.
|
||||
*/
|
||||
private const val REINDEX_DELAY_MS = 500L
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue